Skip to content

Conversation

@FaserF
Copy link
Owner

@FaserF FaserF commented Jan 16, 2026

Summary by CodeRabbit

  • Documentation

    • Added a comprehensive Intune configuration guide, removed the old Intune doc, clarified String/XML payloads, and added development-server security notes.
  • Bug Fixes & Improvements

    • Safer, PEP 440–compliant version generation with validation/fallback, improved installer discovery, clearer error logging, and version string update.
  • UI Refinements

    • Hardening of notification drawer, GitHub login flow, language switching, winget/analyzer layouts, and general UI resilience.
  • Tests

    • Large new UI/integration test suite for buttons, login, language, winget, loading, and notifications.
  • CI / Workflows

    • Workflow gating, Python matrix updates, preview path tweak, and auto-merge pre-check.
  • New Files

    • Added lightweight stacks metadata and security docs.

✏️ Tip: You can customize this high-level summary in your review settings.

@FaserF FaserF self-assigned this Jan 16, 2026
@github-actions github-actions bot added documentation Improvements or additions to documentation ci-cd backend labels Jan 16, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Adopts PEP 440 version suffixes with validation and fallback; updates release/workflow scripts and build installer discovery; hardens many modern GUI views with main-thread marshaling and safer dialog/drawer handling; standardizes Intune docs; and adds extensive UI tests and test fixtures.

Changes

Cohort / File(s) Summary
Version & Release
\.github/scripts/generate_release_version.py, \.github/workflows/release.yml, pyproject.toml, src/switchcraft/__init__.py
Switch to PEP 440 suffixes (e.g., .dev0+sha, b1), validate generated versions, add FALLBACK_VERSION and try/except fallback, update GHA outputs, and bump stored project version strings.
CI / Workflows / Orchestration
\.github/workflows/*.yml (release.yml, ci.yml, test.yml, ci-orchestrator.yml, review-auto-merge.yml, docs_preview.yml)
Update Python matrix (3.14/3.15), add per-run should_run gating, per-step timeouts, no-auto-merge pre-check/duplicate-comment guard, docs preview pages-base-path, and other workflow gating/conditions.
Installer & Build Scripts
scripts/build_release.ps1
Centralized Inno Setup discovery via Get-InnoSetupPath, env-overridable fallback versions, dynamic version parsing, improved messaging and CI-aware guidance.
GUI Core & App Flow
src/switchcraft/gui_modern/app.py, src/switchcraft/modern_main.py
Centralized splash termination (_terminate_splash), safer drawer/dialog open-close flows with guards and logging, and safer subprocess explorer invocation.
View Utilities & Marshaling
src/switchcraft/gui_modern/utils/view_utils.py
Added _run_task_with_fallback to prefer page.run_task with coroutine-aware fallback; improved page resolution and unified error/snack handling.
Settings & Addon Wiring
src/switchcraft/gui_modern/views/settings_view.py
Reworked GitHub device-flow into non-blocking multi-stage flow with background polling, stronger dialog lifecycle handling, added AddonService import and passing, and language-change reload with guarded main-thread updates; _create_addon_row signature updated.
Winget / Intune / Related Views
src/switchcraft/gui_modern/views/winget_view.py, src/switchcraft/gui_modern/views/intune_store_view.py, src/switchcraft/gui_modern/views/intune_view.py
Introduced _run_ui_update/_update_ui helpers to marshal UI updates to main thread, improved timeout/error handling, async logo loading, and consistent error_msg capture for closures.
Group / Stack / Analyzer Views
src/switchcraft/gui_modern/views/group_manager_view.py, src/switchcraft/gui_modern/views/stack_manager_view.py, src/switchcraft/gui_modern/views/analyzer_view.py, src/switchcraft/gui_modern/views/category_view.py, src/switchcraft/gui_modern/views/wingetcreate_view.py
Scoped dialogs to local variables, adjusted layout containers for expansion/alignment, and marshaled UI updates to main thread with guards.
Services
src/switchcraft/services/intune_service.py, src/switchcraft/services/notification_service.py
Preserve original Timeout/RequestException types by re-raising with context; tightened exception clauses (no bare except) and clearer logging.
Docs: Intune / Security / Dev
docs/Intune_Configuration_Guide.md, docs/INTUNE.md, docs/PolicyDefinitions/README.md, docs/IntuneConfig.md (deleted), docs/SECURITY.md, docs/.vitepress/config.mts, docs/package.json, docs/building.md
Add comprehensive Intune configuration guide, remove legacy IntuneConfig.md, standardize OMA‑URI payloads to String/XML with CDATA, add development-server security guidance, set Vite dev server CORS=false, and bump docs deps + esbuild override.
Data
data/stacks.json
Added empty data/stacks.json.
Tests & Fixtures
tests/*, tests/conftest.py
Large additions/updates of UI tests and fixtures (mock_page, CI helpers, poll_until, skip_if_ci) covering buttons, GitHub login flows, language-change, Intune search timeouts/errors, winget details, notification drawer, loading screen, and other UI interactions.
Minor / Versioning Adjustments
pyproject.toml, src/switchcraft/__init__.py
Version bump to PEP‑440 format 2026.1.2.dev0+9d07a00; removed explicit requests dependency entry.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SettingsView as ModernSettingsView
    participant MainThread as Main Thread
    participant AuthService as AuthService
    participant BGThread as Background Thread
    participant Dialog as AlertDialog

    User->>SettingsView: Click "GitHub Login"
    SettingsView->>MainThread: show loading dialog (rgba(0,128,0,0.5))
    SettingsView->>BGThread: init device flow (background)
    BGThread->>AuthService: request device flow
    AuthService-->>BGThread: device_code + verification_uri
    BGThread-->>SettingsView: flow initialized
    SettingsView->>MainThread: show login dialog with codes (rgba(0,128,255,0.5))
    User->>AuthService: Authorize via verification_uri
    SettingsView->>BGThread: start token polling
    BGThread->>AuthService: poll for token
    AuthService-->>BGThread: access token
    BGThread-->>SettingsView: token received
    SettingsView->>MainThread: close dialog, save token, refresh UI (rgba(0,255,0,0.5))
Loading
sequenceDiagram
    actor User
    participant IntuneView as IntuneStoreView
    participant MainThread as Main Thread
    participant SearchThread as Search Thread
    participant IntuneService as IntuneService
    participant Timeout as Timeout Handler

    User->>IntuneView: Trigger search
    IntuneView->>MainThread: show loading UI (rgba(255,165,0,0.5))
    IntuneView->>SearchThread: start _search_task
    SearchThread->>IntuneService: search_apps(query)
    alt success
        IntuneService-->>SearchThread: app results
        SearchThread-->>IntuneView: results
    else timeout
        Timeout->>SearchThread: mark timeout
        SearchThread-->>IntuneView: propagate Timeout
    else network error
        IntuneService-->>SearchThread: RequestException
        SearchThread-->>IntuneView: propagate RequestException
    end
    IntuneView->>MainThread: _update_ui via run_task or fallback (rgba(0,0,255,0.5))
    MainThread->>User: display results or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I nudged the versions, tidy and spry,

When generators wobble, a fallback's nearby.
Dialogs now listen on the main-thread floor,
Threads hum in order, and tests knock at the door.
🥕 Hooray — releases and rabbits want more!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'bug fixes for new features' is vague and generic, using non-descriptive terms that do not convey meaningful information about the changeset's primary focus. Replace with a more specific title that describes the main change, such as 'Add PEP 440 version compliance and improve UI error handling' or 'Refactor version generation and enhance settings/notification flows'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 16, 2026

PR Preview Action v1.8.0

🚀 View preview at
https://FaserF.github.io/SwitchCraft/pr-preview/pr-47/

Built to branch gh-pages at 2026-01-19 16:16 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@docs/INTUNE.md`:
- Around line 18-21: Remove the stray character "w" that appears before the
"[!IMPORTANT]" Intune OMA-URI section in INTUNE.md; open the block containing
the "[!IMPORTANT]" heading (the Intune OMA-URI Configuration text) and delete
the lone "w" so the document starts directly with the "[!IMPORTANT]" directive
and the following lines ("Intune OMA-URI Configuration" and "Critical: Ensure
you use the String (XML) Data Type...").

In `@src/switchcraft/gui_modern/views/intune_store_view.py`:
- Around line 214-224: Reverse the current fallback logic: instead of calling
_update_ui() directly and using run_task as a fallback, use
self.app_page.run_task(_update_ui) as the primary method to marshal UI updates
to the page event loop (when hasattr(self.app_page, 'run_task')). If run_task is
not available, then fallback to calling _update_ui() inside a try/except and log
exceptions with logger.exception; remove or update the misleading "Flet's
update() is thread-safe" comment. Ensure errors from run_task are also caught
and logged (use logger.exception around the run_task call).

In `@tests/test_all_buttons.py`:
- Line 240: The assertion assert len(buttons) >= 0 is meaningless because list
length is never negative; update the test in tests/test_all_buttons.py to assert
a real expectation about the buttons variable (for example, check that buttons
is a list and that it contains elements) by replacing the no-op assertion with
meaningful checks such as verifying type (isinstance(buttons, list)) and/or that
len(buttons) > 0 or another domain-specific condition that ensures the view
created the expected buttons.
- Around line 115-140: The test test_all_buttons_have_handlers improperly calls
another test function test_all_views_have_buttons(), creating a fragile
dependency, and ends with assert True which makes the test meaningless; refactor
by extracting the shared logic into a pure helper (e.g., collect_all_buttons or
a pytest fixture) that returns the all_buttons dict, update
test_all_buttons_have_handlers to call that helper (not the test function),
compute buttons_without_handlers from all_buttons, log them using the existing
print block, and then assert that buttons_without_handlers is empty (e.g.,
assert not buttons_without_handlers, f"Buttons without handlers:
{buttons_without_handlers}") or use pytest.skip/xfail for known exceptions;
update references to test_all_views_have_buttons and buttons_without_handlers
accordingly.

In `@tests/test_intune_store_search.py`:
- Around line 133-172: The test test_intune_search_timeout_mechanism ends with a
trivial assert True and must be changed to actually validate the timeout
behavior: update the test to either (A) assert that the view's timeout handling
was invoked by checking that the mocked view._show_error (or error_calls)
contains the expected timeout message after simulating the long-running
mock_intune_service.search_apps, or (B) refactor ModernIntuneStoreView (or
_run_search) to accept an injectable timeout value so the test can set a short
timeout and then assert that view._show_error was called and search_completed
did not occur; if you prefer not to test timeouts here, remove the test
entirely. Reference: test_intune_search_timeout_mechanism,
ModernIntuneStoreView, view._run_search, mock_intune_service.search_apps, and
view._show_error.
- Around line 36-71: The test test_intune_search_shows_timeout_error is
ineffective: patch threading.Thread.join before calling
ModernIntuneStoreView._run_search so the started search thread uses the mocked
join, simulate a join timeout (e.g., mock_join.return_value or side_effect to
return immediately/raise or no-op to mimic timeout behavior), remove the final
pass and add assertions that view._show_error was invoked with the expected
timeout message (inspect error_calls or assert a call to _show_error), and
ensure the slow_search replacement on mock_intune_service.search_apps and
list_apps remains in place so the timeout path is exercised.

In `@tests/test_language_change.py`:
- Around line 65-67: The test's assertion expecting "restart" can fail because
_on_lang_change in settings_view.py shows "Language changed. UI updated." when
switchcraft_app is present; update the test (using snack_calls) to assert the
actual success message variant instead of always checking for "restart" — for
example, check that any snack contains either "language changed" or "ui updated"
(or allow both the "restart" fallback and the "Language changed. UI updated."
happy path) so the assertion works for both the mock-provided switchcraft_app
path in _on_lang_change and the fallback.

In `@tests/test_notification_bell.py`:
- Around line 50-71: The test test_notification_bell_toggles_drawer should
accept that closing the drawer may set mock_page.end_drawer to None; update the
final assertion (after calling ModernApp._toggle_notification_drawer the second
time) to allow either end_drawer being None or its open attribute being False
(e.g., assert mock_page.end_drawer is None or mock_page.end_drawer.open is
False) so the test passes whether the implementation clears the reference or
merely closes it.
🧹 Nitpick comments (20)
docs/Intune_Configuration_Guide.md (1)

194-194: Consider hyphenating "Top-Level" as a compound adjective.

Minor style suggestion: "Top Level Settings" should be "Top-Level Settings" for grammatical correctness when used as a compound modifier.

✏️ Proposed fix
-### Top Level Settings
+### Top-Level Settings
scripts/build_release.ps1 (2)

332-352: Expanded Inno Setup discovery is good, but contains redundant paths.

The path array includes both hardcoded paths (e.g., "C:\Program Files (x86)\Inno Setup 6\ISCC.exe") and environment variable paths (e.g., "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe") that typically resolve to the same location. This isn't a functional issue—just minor redundancy that adds a few extra Test-Path calls.

♻️ Optional: Remove redundant hardcoded paths
         $PossiblePaths = @(
-            "C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
-            "C:\Program Files\Inno Setup 6\ISCC.exe",
-            "C:\Program Files (x86)\Inno Setup 5\ISCC.exe",
-            "C:\Program Files\Inno Setup 5\ISCC.exe",
             "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
             "$env:ProgramFiles\Inno Setup 6\ISCC.exe",
             "${env:ProgramFiles(x86)}\Inno Setup 5\ISCC.exe",
-            "$env:ProgramFiles\Inno Setup 5\ISCC.exe"
+            "$env:ProgramFiles\Inno Setup 5\ISCC.exe",
+            # Fallback for edge cases where env vars aren't set
+            "C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
+            "C:\Program Files\Inno Setup 6\ISCC.exe"
         )

406-422: Duplicated Inno Setup discovery logic.

This path discovery block is identical to lines 332-349. Consider extracting this into a helper function to reduce duplication and ensure consistent behavior (e.g., the "Found Inno Setup at" message is missing here).

♻️ Extract helper function for Inno Setup discovery

Add this helper function near the other helper functions (around line 205):

function Find-InnoSetup {
    $IsccPath = (Get-Command "iscc" -ErrorAction SilentlyContinue).Source
    if ($IsccPath) {
        Write-Host "Found Inno Setup in PATH: $IsccPath" -ForegroundColor Gray
        return $IsccPath
    }
    
    $PossiblePaths = @(
        "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
        "$env:ProgramFiles\Inno Setup 6\ISCC.exe",
        "${env:ProgramFiles(x86)}\Inno Setup 5\ISCC.exe",
        "$env:ProgramFiles\Inno Setup 5\ISCC.exe"
    )
    foreach ($p in $PossiblePaths) {
        if (Test-Path $p) {
            Write-Host "Found Inno Setup at: $p" -ForegroundColor Gray
            return $p
        }
    }
    return $null
}

Then replace both discovery blocks with:

$IsccPath = Find-InnoSetup
.github/workflows/release.yml (1)

41-50: Good defense-in-depth version validation.

The shell-level validation complements the Python script's built-in fallback, providing an extra safety net. The regex correctly validates the basic MAJOR.MINOR.PATCH format.

Note: The hardcoded fallback "2026.1.2" here must be kept in sync with FALLBACK_VERSION in generate_release_version.py. Consider defining this as a workflow-level environment variable to ensure consistency.

♻️ Optional: Define fallback as workflow-level env var
 on:
   workflow_dispatch:
     inputs:
       release_type:
         # ...
 
+env:
+  FALLBACK_VERSION: "2026.1.2"
+
 permissions:
   contents: write

Then reference as ${{ env.FALLBACK_VERSION }} in the validation step.

src/switchcraft/services/intune_service.py (1)

394-403: Remove the duplicate RequestException handler.
The second block is unreachable and leaves conflicting error semantics.

♻️ Suggested cleanup
         except requests.exceptions.RequestException as e:
             logger.error(f"Network error in list_apps: {e}")
             raise
-        except requests.exceptions.RequestException as e:
-            logger.error(f"Network error while listing apps: {e}")
-            raise Exception(f"Network error: {str(e)}")
tests/test_github_login.py (2)

61-64: Remove unused get_page function definitions.

These get_page functions are defined but never used—only the property assignment on line 64/102/146 is effective. The function definitions are dead code.

♻️ Suggested cleanup
-    # Mock the page property to avoid RuntimeError
-    def get_page():
-        return mock_page
     type(view).page = property(lambda self: mock_page)

Also applies to: 99-102, 143-146


72-73: Consider using pytest-timeout or event-based synchronization instead of time.sleep.

Using fixed time.sleep(0.5) calls for async synchronization can lead to flaky tests—too short may cause failures on slow CI, too long wastes time. Consider using threading Events or polling with shorter sleeps and a max retry count for more reliable tests.

Also applies to: 116-117, 167-168

src/switchcraft/gui_modern/app.py (2)

188-205: Retry logic may cause redundant operations.

The retry path at lines 195-197 calls _open_notifications_drawer(e) again after it already failed in the try block (line 189). If the failure is deterministic (e.g., missing permissions, invalid state), retrying immediately won't help and wastes resources. Consider adding a different recovery strategy or removing the redundant retry.

♻️ Suggested simplification
             try:
                 self._open_notifications_drawer(e)
                 # Force update to ensure drawer is visible
                 self.page.update()
             except Exception as ex:
                 logger.exception(f"Error opening notification drawer: {ex}")
-                # Try again with simpler approach
-                try:
-                    self._open_notifications_drawer(e)
-                    self.page.update()
-                except Exception as ex2:
-                    logger.error(f"Failed to open drawer after retry: {ex2}")
-                    self.page.snack_bar = ft.SnackBar(
-                        content=ft.Text(f"Failed to open notifications: {ex2}"),
-                        bgcolor="RED"
-                    )
-                    self.page.snack_bar.open = True
-                    self.page.update()
+                self.page.snack_bar = ft.SnackBar(
+                    content=ft.Text(f"Failed to open notifications: {ex}"),
+                    bgcolor="RED"
+                )
+                self.page.snack_bar.open = True
+                self.page.update()

1006-1009: Triple page.update() calls are unusual.

Calling page.update() three times consecutively suggests potential underlying rendering issues. While this might work around a Flet quirk, it's a code smell. Consider investigating the root cause or adding a comment explaining why this is necessary.

tests/test_intune_store_search.py (1)

19-21: Remove unused get_page function.

The get_page function is defined but never called—only the property assignment matters.

♻️ Suggested cleanup
-    # Mock page property to avoid RuntimeError
-    def get_page():
-        return page
     type(page).page = property(lambda self: page)
tests/test_winget_details.py (1)

19-21: Remove unused get_page function.

Same issue as in other test files—the function is defined but never called.

♻️ Suggested cleanup
-    # Mock page property to avoid RuntimeError
-    def get_page():
-        return page
     type(page).page = property(lambda self: page)
tests/test_all_buttons.py (3)

41-46: Mock page is missing run_task and other attributes needed by some views.

Compared to other test fixtures in the codebase (e.g., test_button_functionality.py lines 12-28), this mock is missing run_task, dialog, end_drawer, snack_bar, open, and close. Some views like ModernIntuneStoreView and ModernWingetView may fail or behave unexpectedly during instantiation.

Suggested enhancement
     mock_page = MagicMock(spec=ft.Page)
     mock_page.update = MagicMock()
     mock_page.switchcraft_app = MagicMock()
     mock_page.switchcraft_app.goto_tab = MagicMock()
     mock_page.switchcraft_app._current_tab_index = 0
+    mock_page.switchcraft_app._view_cache = {}
+    mock_page.dialog = None
+    mock_page.end_drawer = None
+    mock_page.snack_bar = None
+    mock_page.open = MagicMock()
+    mock_page.close = MagicMock()
+    mock_page.run_task = lambda func: func()  # Execute immediately for testing
     type(mock_page).page = property(lambda self: mock_page)

143-145: Same test coupling issue as above.

test_button_handlers_are_callable also calls test_all_views_have_buttons() directly, creating the same fragile dependency.


30-112: Test function acts as both test and data provider - consider refactoring.

test_all_views_have_buttons returns all_buttons (line 112), which is used by other tests. However, pytest ignores test return values. This dual-purpose design leads to the coupling issues noted above.

Consider extracting the view instantiation and button discovery logic into a pytest fixture that can be reused across tests.

src/switchcraft/gui_modern/views/settings_view.py (2)

703-709: Redundant close dialog wrapper.

The close_dlg function wraps _close and uses run_task if available, but since _show_dialog_with_flow is already called from the main thread (via run_task at line 792), the extra run_task check is unnecessary here.


1058-1068: Direct call with run_task fallback is reasonable but inconsistent with other patterns.

The comment states "Flet's update() is thread-safe" (line 1058), so calling _reload_app() directly is attempted first. However, other parts of this file (e.g., _start_github_login) use run_task as the primary path. Consider keeping the pattern consistent across the codebase for maintainability.

tests/test_language_change.py (1)

7-7: Unused import.

time is imported but not used in this file.

-import time
tests/test_button_functionality.py (3)

8-9: Unused imports.

inspect and importlib are imported but not used in this file.

-import inspect
-import importlib

42-49: Duplicated find_clickables helper function.

The same helper function is defined twice (lines 42-49 and 86-93). Extract it to module level or as a fixture to reduce duplication.

Extract to module level
+def find_clickables(control, clickables):
+    """Recursively find all controls with on_click handlers."""
+    if hasattr(control, 'on_click') and control.on_click is not None:
+        clickables.append(control)
+    if hasattr(control, 'content'):
+        find_clickables(control.content, clickables)
+    if hasattr(control, 'controls'):
+        for child in control.controls:
+            find_clickables(child, clickables)
+

 def test_home_view_buttons(mock_page):
     """Test that Home View buttons work."""
     from switchcraft.gui_modern.views.home_view import ModernHomeView

     navigate_calls = []
     def track_navigate(idx):
         navigate_calls.append(idx)

     view = ModernHomeView(mock_page, on_navigate=track_navigate)

-    # Find all clickable containers
-    def find_clickables(control, clickables):
-        if hasattr(control, 'on_click') and control.on_click is not None:
-            clickables.append(control)
-        if hasattr(control, 'content'):
-            find_clickables(control.content, clickables)
-        if hasattr(control, 'controls'):
-            for child in control.controls:
-                find_clickables(child, clickables)
-
     clickables = []
     find_clickables(view, clickables)

Also applies to: 86-93


175-175: Weak assertions: assert True provides no verification.

Both test_winget_view_search_button and test_intune_store_search_button end with assert True, which always passes and doesn't verify any behavior.

Consider verifying that the search was triggered or that the view state changed:

-        assert True  # Just verify view was created
+        # Verify view was created and search can be triggered
+        assert view is not None

Or remove the assertion entirely if the test's purpose is just to ensure no exceptions are raised during the flow.

Also applies to: 200-200

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52b2420 and c65f7ff.

📒 Files selected for processing (24)
  • .github/scripts/generate_release_version.py
  • .github/workflows/release.yml
  • data/stacks.json
  • docs/INTUNE.md
  • docs/IntuneConfig.md
  • docs/Intune_Configuration_Guide.md
  • docs/PolicyDefinitions/README.md
  • scripts/build_release.ps1
  • src/switchcraft/gui_modern/app.py
  • src/switchcraft/gui_modern/views/category_view.py
  • src/switchcraft/gui_modern/views/intune_store_view.py
  • src/switchcraft/gui_modern/views/settings_view.py
  • src/switchcraft/gui_modern/views/winget_view.py
  • src/switchcraft/services/intune_service.py
  • switchcraft_legacy.iss
  • switchcraft_modern.iss
  • tests/test_all_buttons.py
  • tests/test_button_functionality.py
  • tests/test_github_login.py
  • tests/test_intune_store_search.py
  • tests/test_language_change.py
  • tests/test_loading_screen.py
  • tests/test_notification_bell.py
  • tests/test_winget_details.py
💤 Files with no reviewable changes (1)
  • docs/IntuneConfig.md
🧰 Additional context used
🧬 Code graph analysis (8)
tests/test_language_change.py (1)
src/switchcraft/gui_modern/views/settings_view.py (2)
  • ModernSettingsView (17-1765)
  • _on_lang_change (968-1155)
tests/test_intune_store_search.py (1)
src/switchcraft/gui_modern/views/intune_store_view.py (5)
  • ModernIntuneStoreView (14-623)
  • _get_token (119-125)
  • _show_error (228-241)
  • _run_search (127-226)
  • _update_list (243-286)
src/switchcraft/gui_modern/views/settings_view.py (3)
src/switchcraft/services/auth_service.py (3)
  • initiate_device_flow (27-44)
  • poll_for_token (47-90)
  • save_token (93-95)
src/switchcraft/gui_modern/utils/view_utils.py (1)
  • _show_snack (9-20)
tests/test_github_login.py (1)
  • run_task (21-22)
tests/test_notification_bell.py (1)
src/switchcraft/gui_modern/app.py (1)
  • _toggle_notification_drawer (140-222)
tests/test_button_functionality.py (5)
src/switchcraft/gui_modern/views/home_view.py (1)
  • ModernHomeView (6-156)
src/switchcraft/gui_modern/views/category_view.py (1)
  • CategoryView (41-116)
src/switchcraft/gui_modern/views/settings_view.py (1)
  • ModernSettingsView (17-1765)
src/switchcraft/gui_modern/views/winget_view.py (1)
  • ModernWingetView (13-1033)
src/switchcraft/gui_modern/views/intune_store_view.py (2)
  • ModernIntuneStoreView (14-623)
  • _get_token (119-125)
src/switchcraft/gui_modern/views/intune_store_view.py (2)
src/switchcraft/utils/i18n.py (1)
  • get (179-206)
src/switchcraft/gui/views/intune_store_view.py (1)
  • _show_error (106-109)
src/switchcraft/gui_modern/views/winget_view.py (1)
src/switchcraft/debug_views.py (1)
  • update (19-19)
tests/test_loading_screen.py (3)
src/switchcraft/gui_modern/app.py (1)
  • main (1814-1824)
tests/test_github_login.py (1)
  • mock_page (12-33)
src/switchcraft/debug_views.py (5)
  • add (20-20)
  • update (19-19)
  • width (28-28)
  • height (30-30)
  • open (22-22)
🪛 LanguageTool
docs/Intune_Configuration_Guide.md

[style] ~3-~3: Try moving the adverb to make the sentence clearer.
Context: ...uration Guide This guide describes how to correctly configure SwitchCraft policies using Microsoft Intune Custom Profiles ...

(SPLIT_INFINITIVE)


[uncategorized] ~194-~194: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...HUMBPRINT_HEX_STRING"/> ``` --- ### Top Level Settings #### 1. Debug Mode (`DebugMod...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🪛 markdownlint-cli2 (0.18.1)
docs/PolicyDefinitions/README.md

53-53: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test-backend / test-backend (3.13)
🔇 Additional comments (43)
data/stacks.json (1)

1-1: LGTM!

Valid initialization file for the Stack Manager feature. The empty JSON object provides a proper starting state for storing stack configurations, as referenced in the documentation.

switchcraft_legacy.iss (1)

8-13: LGTM!

Version constants updated consistently to 2026.1.2. The #ifndef guards properly allow CI/CD override while providing sensible defaults.

switchcraft_modern.iss (1)

8-13: LGTM!

Version constants updated consistently to 2026.1.2, matching the legacy installer.

docs/Intune_Configuration_Guide.md (1)

1-202: Well-structured Intune configuration guide.

The documentation is comprehensive, covering common errors, prerequisites, and all OMA-URI settings with proper XML payload examples. This will be valuable for enterprise deployments.

docs/PolicyDefinitions/README.md (3)

63-76: LGTM! Clear and consistent table structure.

The Configuration Reference table effectively communicates the String data type requirement with consistent formatting and helpful XML examples for each policy setting.


79-79: LGTM! Consistent XML examples throughout.

The XML examples consistently use String DataType with properly CDATA-wrapped payloads, aligning with the guidance in the IMPORTANT note and Configuration Reference table.

Also applies to: 93-94, 107-108, 170-171


47-52: Technical guidance verified as accurate per Microsoft documentation.

The documentation correctly states that ADMX-backed policies in Intune must use String (or String (XML)) data type with XML-encoded payloads, never Integer or Boolean. This aligns with Microsoft Learn and official Intune support guidance on OMA-URI data type requirements. The IMPORTANT note appropriately emphasizes this critical configuration requirement.

.github/scripts/generate_release_version.py (2)

6-6: LGTM!

The sys import is correctly added to support stderr output for warnings and errors.


74-88: Good defensive error handling with consistent fallback.

The try/except pattern with validation provides robust fallback behavior. Using sys.stderr for warnings/errors while keeping stdout clean for version output is the correct approach for CI pipelines.

One minor note: the regex r'^\d+\.\d+\.\d+' (without end anchor $) will match versions like 1.2.3-beta and 1.2.3-dev-abc123 which is correct given the calculate_next_version function produces such suffixes.

scripts/build_release.ps1 (2)

155-174: LGTM!

The fallback version handling is well-structured: defaults are set upfront, and all failure modes (parse failure, missing file) are handled with informative warnings.


380-393: Read-Host may hang in CI/CD if Inno Setup is missing and winget is available.

The Read-Host prompt only executes when Inno Setup is not found but winget is available. Since GitHub Actions Windows runners don't reliably pre-install Inno Setup, this interactive prompt could block CI/CD pipelines. For automated workflows, either pre-install Inno Setup or conditionally disable the Installer target.

For local development, the current behavior is acceptable and provides helpful UX.

.github/workflows/release.yml (2)

169-175: Automated fallback version propagation is well-designed.

The sed commands correctly escape the $ characters for shell interpolation. The approach of calculating FALLBACK_VERSION from the release version ensures consistency.

One consideration: the sed pattern for build_release.ps1 matches \$AppVersion = \".*\" which could theoretically match multiple lines if there were other variables with similar names. In the current script, this appears safe since $AppVersion and $AppVersionNumeric are the only matches on lines 156-157.


177-180: LGTM!

The git operations correctly stage all modified files, and the commit message clearly documents both the development version and fallback version updates. The [skip ci] tag appropriately prevents triggering another workflow run.

src/switchcraft/gui_modern/views/category_view.py (1)

90-94: LGTM on the explicit alignment.
Keeps the icon centered while making the positioning explicit.

Please sanity-check against the current Flet alignment API.

tests/test_loading_screen.py (1)

10-53: Good coverage for loading-screen presence.
Catches regressions in initial add/update behavior and basic loading/progress markers.

Please confirm the assertions still match the current main() initialization flow.

src/switchcraft/services/intune_service.py (1)

448-465: Nice propagation of network errors in fallback paths.
Re-raising Timeout/RequestException keeps callers aware of network failures.

Please confirm this aligns with the expected requests exception flow in your runtime.

src/switchcraft/gui_modern/views/winget_view.py (2)

243-245: Good normalization of empty results.
Guarantees stable list handling when the helper returns None.

Please confirm this matches the Winget helper’s current return types.

Also applies to: 277-281


685-712: Details pane refresh flow looks solid.
Rebuilding the Column and reassigning it should force a reliable redraw.

Please sanity-check against current Flet update behavior.

tests/test_notification_bell.py (1)

10-47: Good coverage for opening the notification drawer.
Validates drawer creation/open + update behavior.

Please confirm this still reflects the latest drawer-open flow.

tests/test_github_login.py (4)

11-33: Well-structured mock page fixture.

The fixture correctly mocks run_task to execute synchronously and simulates dialog opening behavior. The pattern of capturing dialog state via mock_open is appropriate for testing dialog-based flows.


43-86: Test coverage for dialog flow looks comprehensive.

The test validates dialog opening, content verification (user code and verification URI), and UI update calls. Good coverage of the happy path for device-flow initiation.


89-121: Error path test is well-structured.

Properly tests the failure scenario when device flow initiation returns None, verifying that an error snack is shown with appropriate messaging.


124-178: Success flow test validates complete token handling.

Good coverage verifying token save, UI update via _update_sync_ui, and success message display. The tracking pattern using wrapper functions is appropriate.

src/switchcraft/gui_modern/app.py (3)

165-172: Good defensive programming for drawer reference handling.

Saving drawer_ref before clearing and using it for subsequent operations prevents issues with stale references and null pointer scenarios.


1225-1230: Defensive drawer open enforcement is reasonable.

Forcing the drawer open flag when it's unexpectedly false is a pragmatic workaround for potential Flet state synchronization issues. The warning log helps with debugging.


1823-1824: Exposing app instance on page enables cross-view communication.

This assignment is already done in __init__ at line 42. Having it in both places ensures the reference is set regardless of how the app is initialized, which is fine for robustness.

src/switchcraft/gui_modern/views/intune_store_view.py (3)

131-140: Good defensive UI update with fallback.

The try-except pattern that falls back to updating just the results list when the full view update fails is a reasonable approach to handle partially-attached control states.


185-186: Appropriate timeout logging.

Warning log when the search thread times out helps with debugging slow network conditions or hung requests.


195-199: Good defensive completion state handling.

Ensuring completed is set and providing a default error message when no response is received prevents the UI from getting stuck in a loading state indefinitely.

tests/test_intune_store_search.py (2)

74-98: Network error test validates error handling correctly.

Good test structure: mocks the exception, triggers the search, and asserts that the error callback receives an appropriate message.


101-130: Success path test properly validates result handling.

Correctly tests that search results are passed to _update_list with the expected structure and content.

tests/test_winget_details.py (4)

37-89: Comprehensive test for details loading and display.

Good coverage: validates details callback, control structure, and right pane visibility. The wrapper pattern for tracking _show_details_ui calls is appropriate.


92-119: Loading state test correctly validates immediate UI feedback.

Verifying that a ProgressBar appears immediately and the right pane becomes visible before async loading completes is good UX validation.


145-156: Clever recursive control traversal for error text detection.

The collect_text helper recursively traverses nested controls to find error messages, which handles Flet's nested container structure well. This is a good testing pattern.


159-203: UI update tracking test validates rendering calls.

Good test structure: wraps both view and page update methods to verify that UI updates are triggered during the details loading flow.

tests/test_all_buttons.py (1)

14-27: LGTM! Recursive button discovery helper is well-structured.

The helper correctly handles multiple Flet button types and traverses controls, content, and actions attributes.

src/switchcraft/gui_modern/views/settings_view.py (4)

672-695: Background flow initiation with proper error handling - LGTM.

The _init_flow function correctly uses run_task to dispatch UI error notifications to the main thread when available, with a fallback for direct calls.


757-761: Defensive dialog open check is good.

The check at lines 757-761 ensures the dialog is actually opened even if the initial attempt didn't set the flag correctly. This is good defensive programming.


764-783: Token polling and UI update on main thread - well implemented.

The polling runs in a background thread and correctly uses run_task to dispatch UI updates (dialog close, snackbar) to the main thread.


786-809: Good orchestration with proper thread-safety fallbacks.

The _flow_complete function properly sequences: (1) background network call, (2) main-thread dialog display via run_task, with fallback to direct call if run_task is unavailable.

tests/test_language_change.py (1)

25-45: Test correctly verifies config and i18n updates.

The test properly patches dependencies and verifies that set_user_preference and set_language are called with the expected arguments.

tests/test_button_functionality.py (2)

12-28: Well-structured mock page fixture.

This fixture includes all necessary attributes (run_task, dialog, open, close, etc.) that views may depend on. Good alignment with other test files in the codebase.


137-175: Test correctly mocks AddonService and WingetHelper chain.

The patching correctly sets up the mock chain for AddonService().import_addon_module().WingetHelper(). The search for the button and conditional click handling is defensive and appropriate.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/switchcraft/services/intune_service.py (1)

484-489: Inconsistent exception handling: wraps Timeout in generic Exception.

In list_apps (line 396), Timeout is re-raised as itself, but here it's wrapped in a generic Exception. This inconsistency prevents callers from catching Timeout specifically.

🐛 Suggested fix for consistency
         except requests.exceptions.Timeout:
             logger.error(f"Request timed out while getting app details for {app_id}")
-            raise Exception("Request timed out. The server took too long to respond.")
+            raise requests.exceptions.Timeout("Request timed out. The server took too long to respond.")
         except requests.exceptions.RequestException as e:
             logger.error(f"Network error getting app details: {e}")
-            raise Exception(f"Network error: {str(e)}")
+            raise
src/switchcraft/gui_modern/views/winget_view.py (1)

376-416: Remove incorrect thread-safety assumption and use page.run_task() as the primary approach for UI updates from background threads.

The comment "Flet's update() is thread-safe" contradicts Flet's official documentation. Since _fetch() runs in a background thread (line 419: threading.Thread(target=_fetch, daemon=True).start()), calling update() directly can cause race conditions. Flet requires UI mutations to be marshalled back to the main/app context.

Reverse the logic: use page.run_task(_show_ui) as the primary path and remove the direct call, or remove the direct call entirely in favor of run_task.

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Around line 28-35: The job-level if currently references the matrix context
(matrix.python-version) which isn’t available at job evaluation; remove that
matrix-based condition from the job if and either (a) split the Python 3.15 run
into a separate job (e.g., python-3-15) with its own job-level if guarded by
github.event_name == 'push' && github.ref == 'refs/heads/main', or (b) keep the
matrix and move the 3.15-specific gating into step-level if checks;
additionally, when creating the separate 3.15 job (or when adding 3.15 to
setup-python), update the actions/setup-python@v6 usage to include with:
allow-prereleases: true so prerelease Python 3.15 is accepted.

In @.github/workflows/release.yml:
- Around line 41-50: The VERSION validation uses an unanchored grep which lets
malformed strings like "1.2.3.4" or "1.2.3foo" pass; update the grep check that
references the VERSION variable in the if condition to use a strict anchored
regex that matches only MAJOR.MINOR.PATCH and optional pre-release/build
identifiers (e.g. anchor with ^ and $ and allow an optional -pre and +build
suffix pattern), so the fallback to "2026.1.2" triggers for any invalid format.

In @.github/workflows/test.yml:
- Around line 13-23: The job-level if currently references matrix.python-version
(and uses matrix in the job if), which is invalid; instead either (A) move the
Python-version-specific logic into step-level conditions by keeping the matrix
in the job and adding an if on the test step (use matrix.python-version and
github.event_name/github.ref inside the step's if), or (B) encode the allowed
combinations using the matrix include/exclude feature in the matrix definition
so the job-level if only uses allowed contexts (e.g., inputs/component); update
references to matrix.python-version in the job-level if to be removed and place
them on the relevant step's if or into the matrix include/exclude block to
restrict Python 3.15 runs to main push events.

In `@scripts/build_release.ps1`:
- Around line 377-395: The script currently uses Read-Host (at the $response
prompt) which blocks non-interactive CI; wrap the interactive prompt and
subsequent winget install attempt in a guard that only runs for
local/interactive sessions (e.g. check that neither $env:CI nor
$env:GITHUB_ACTIONS is set), so replace the current unconditional Read-Host
block with an if (-not $env:CI -and -not $env:GITHUB_ACTIONS) { $response =
Read-Host ...; if ($response -in 'y','Y') { try { winget install --id
JRSoftware.InnoSetup ... } catch { Write-Warning ... } } } else { Write-Host
"Skipping interactive Inno Setup install in CI." -ForegroundColor Yellow } to
ensure $wingetAvailable, Read-Host and the winget install are only executed
interactively.

In `@src/switchcraft/gui_modern/views/intune_store_view.py`:
- Line 131: The current call to i18n.get("searching") can return the key itself
when a translation is missing, so the "or" fallback won't work; update the code
that appends to self.results_list.controls (the ft.Text creation) to pass a
default/fallback directly to i18n.get (e.g., use the i18n.get signature that
accepts a default like i18n.get("searching", "Searching...")) so ft.Text always
receives a human-friendly string even when the translation is absent.

In `@tests/test_all_three_issues.py`:
- Around line 182-186: The loop that assigns lang_dd by iterating only
general_tab.controls can miss nested Dropdowns; change the lookup to perform a
recursive search through general_tab's descendants (same approach used in
test_language_change_updates_ui) to find the first ft.Dropdown, so that lang_dd
is set even when the dropdown is nested instead of a direct child of
general_tab.controls.

In `@tests/test_intune_store_search.py`:
- Around line 35-88: The test test_intune_search_shows_timeout_error has
redundant local imports and a race/global-patch issue: remove the local imports
of os and threading, stop globally reassigning threading.Thread, and ensure the
mocked thread accurately simulates a running thread so slow_search is never
invoked; instead use a local Thread subclass or context-managed patching to
override join and is_alive for threads created by view._run_search. Concretely,
replace the direct assignment threading.Thread = mock_thread with a scoped
monkeypatch/patch of threading.Thread (or use unittest.mock.patch) when creating
mock_thread, ensure mock_thread returns a Thread instance whose is_alive returns
True and join returns immediately, and keep slow_search and
view._get_token/view._show_error as-is so the timeout branch in
ModernIntuneStoreView._run_search is exercised without global side effects.
♻️ Duplicate comments (1)
src/switchcraft/gui_modern/views/intune_store_view.py (1)

201-220: Align the UI-thread comment with the run_task path.

The comment says to avoid run_task, but the code now uses run_task as the primary marshalling mechanism. Update/remove the comment to avoid misleading future edits.

🛠️ Suggested fix
-            # Update UI on main thread - MUST use page.update() directly, not run_task
+            # Update UI on the page event loop; prefer run_task when available
🧹 Nitpick comments (11)
docs/Intune_Configuration_Guide.md (1)

1-202: Optional: Consider adding a troubleshooting section.

The guide is comprehensive and well-structured. To further enhance it, consider adding:

  • A troubleshooting section with additional common errors and solutions
  • Guidance on validating that policies are applied correctly
  • Links to related documentation (e.g., the INTUNE.md and PolicyDefinitions/README.md mentioned in the PR context)

These additions would make the guide even more valuable for administrators, but the current content is already solid and production-ready.

src/switchcraft/gui_modern/views/winget_view.py (1)

451-461: Consistent with the pattern above - same thread-safety concern applies.

The same fallback pattern is used here for error UI. Consider consolidating this into a helper method to reduce duplication.

♻️ Suggested helper to reduce duplication
def _run_on_ui_thread(self, func):
    """Execute function on UI thread with fallback."""
    try:
        func()
    except Exception as ex:
        logger.debug(f"Direct call failed: {ex}")
        if hasattr(self.app_page, 'run_task'):
            try:
                self.app_page.run_task(func)
            except Exception:
                pass
tests/test_winget_details.py (2)

70-78: Using time.sleep() for async synchronization is flaky.

Tests that rely on time.sleep() can fail intermittently on slow CI runners or pass despite bugs on fast machines. Consider using polling with a timeout or mocking the threading entirely.

♻️ Suggested improvement using polling
import time

def wait_for(condition, timeout=2.0, interval=0.05):
    """Wait for condition to be true, polling at interval."""
    start = time.time()
    while time.time() - start < timeout:
        if condition():
            return True
        time.sleep(interval)
    return False

# Usage in test:
assert wait_for(lambda: len(details_shown) > 0), "Details should be loaded"

174-196: Test verifies update calls but assertion at line 196 is weak.

The assertion assert len(update_calls) > 0 or len(page_update_calls) > 0 passes if either is called, but doesn't verify that updates happened at the right time or in the right sequence.

tests/test_all_buttons.py (1)

260-264: Swallowing handler exceptions may hide real bugs.

Printing failures but not failing the test means broken handlers go unnoticed. Consider collecting failures and asserting at the end, or using pytest.warns/pytest.raises selectively.

♻️ Suggested improvement to track failures
+        handler_failures = []
         for button in buttons:
             if hasattr(button, 'on_click') and button.on_click is not None:
                 try:
                     mock_event = MagicMock()
                     mock_event.control = button
                     mock_event.data = "true"
                     button.on_click(mock_event)
                 except Exception as e:
-                    print(f"Button handler failed in {view_name}: {e}")
+                    handler_failures.append(f"{view_name}: {e}")
 
         assert view is not None
+        # Optionally: assert not handler_failures, f"Handler failures: {handler_failures}"
tests/test_all_three_issues.py (1)

50-67: Mock AuthService fixture provides complete device flow mocking.

All necessary return values are configured for the GitHub device flow. However, there's redundancy: mock_service is created but only mock is returned.

♻️ Simplify fixture
 `@pytest.fixture`
 def mock_auth_service():
     """Mock AuthService responses."""
     with patch('switchcraft.gui_modern.views.settings_view.AuthService') as mock:
-        mock_service = MagicMock()
         mock.initiate_device_flow.return_value = {
             "device_code": "test_code",
             "user_code": "ABC-123",
             "verification_uri": "https://github.com/login/device",
             "interval": 5,
             "expires_in": 900
         }
         mock.poll_for_token.return_value = "test_token_123"
         mock.save_token = MagicMock()
-        mock_service.initiate_device_flow = mock.initiate_device_flow
-        mock_service.poll_for_token = mock.poll_for_token
-        mock_service.save_token = mock.save_token
-        return mock
+        yield mock
tests/test_ui_interactions_critical.py (1)

85-87: Avoid fixed sleeps in UI tests; poll for state instead.

Fixed waits add flakiness; polling for the dialog/open state (or goto_tab call) makes the tests more deterministic.

♻️ Suggested approach (apply similarly to the other sleep)
-    time.sleep(1.0)
+    for _ in range(20):
+        if mock_page.dialog and mock_page.dialog.open:
+            break
+        time.sleep(0.1)

Also applies to: 136-137

tests/test_github_login_real.py (1)

72-73: Replace fixed sleep with polling to reduce flakiness.

♻️ Suggested fix
-    time.sleep(0.5)
+    for _ in range(10):
+        if mock_page.dialog and mock_page.dialog.open:
+            break
+        time.sleep(0.05)
tests/test_button_functionality.py (2)

56-67: Don’t silently swallow all click failures.

Right now the tests can pass even if every handler crashes. Consider tracking successful clicks and asserting at least one succeeds (apply the same pattern to the other loops).

♻️ Example adjustment (apply similarly to other loops)
-    for clickable in clickables:
+    successful_clicks = 0
+    for clickable in clickables:
         if hasattr(clickable, 'on_click') and clickable.on_click:
             try:
                 mock_event = MagicMock()
                 mock_event.control = clickable
                 clickable.on_click(mock_event)
+                successful_clicks += 1
             except Exception as e:
                 # Some might fail due to missing dependencies, that's OK
                 pass
@@
-    assert len(clickables) > 0
+    assert len(clickables) > 0
+    assert successful_clicks > 0, "Expected at least one clickable handler to succeed"

Also applies to: 91-100, 117-127


167-168: Strengthen the “button works” assertions.

assert True / view is not None won’t catch regressions in button wiring. A minimal improvement is to assert the handler exists and is callable.

♻️ Suggested assertions
-        assert True  # Just verify view was created
+        assert search_button is not None, "Search button should exist"
+        assert callable(search_button.on_click), "Search button must have on_click handler"
@@
-        assert view is not None
+        assert view.btn_search is not None, "Search button should exist"
+        assert callable(view.btn_search.on_click), "Search button must have on_click handler"

Also applies to: 192-192

tests/test_intune_store_search.py (1)

150-216: Redundant imports and potential flakiness in timeout test.

  1. Redundant imports: threading (line 153) and os (line 154) are already imported at module level.

  2. Potential flakiness: The assertion search_started.wait(timeout=1.0) at line 205 may fail intermittently. The search_started.set() is called inside slow_search, but slow_search is assigned to mock_intune_service.search_apps (line 170). The actual _run_search method may call search_apps or list_apps depending on the query value. Since list_apps is not mocked with this behavior in this test, if the implementation path changes, the test could break.

  3. Improved from past review: The test now has meaningful assertions (lines 211-215) instead of assert True.

Remove redundant imports
 def test_intune_search_timeout_mechanism(mock_page, mock_intune_service):
     """Test that Intune search properly times out after 60 seconds."""
     from switchcraft.gui_modern.views.intune_store_view import ModernIntuneStoreView
-    import threading
-    import os
 
     # Skip in CI to avoid 65 second wait
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c65f7ff and fad5331.

📒 Files selected for processing (26)
  • .github/workflows/ci.yml
  • .github/workflows/docs_preview.yml
  • .github/workflows/release.yml
  • .github/workflows/test.yml
  • docs/.vitepress/config.mts
  • docs/INTUNE.md
  • docs/Intune_Configuration_Guide.md
  • scripts/build_release.ps1
  • src/switchcraft/gui_modern/app.py
  • src/switchcraft/gui_modern/views/analyzer_view.py
  • src/switchcraft/gui_modern/views/intune_store_view.py
  • src/switchcraft/gui_modern/views/settings_view.py
  • src/switchcraft/gui_modern/views/stack_manager_view.py
  • src/switchcraft/gui_modern/views/winget_view.py
  • src/switchcraft/gui_modern/views/wingetcreate_view.py
  • src/switchcraft/services/intune_service.py
  • tests/test_all_buttons.py
  • tests/test_all_three_issues.py
  • tests/test_button_functionality.py
  • tests/test_github_login.py
  • tests/test_github_login_real.py
  • tests/test_intune_store_search.py
  • tests/test_language_change.py
  • tests/test_notification_bell.py
  • tests/test_ui_interactions_critical.py
  • tests/test_winget_details.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_language_change.py
  • tests/test_notification_bell.py
  • tests/test_github_login.py
  • docs/INTUNE.md
🧰 Additional context used
🧬 Code graph analysis (9)
tests/test_ui_interactions_critical.py (7)
tests/test_github_login.py (4)
  • mock_page (13-34)
  • run_task (22-23)
  • mock_open (27-31)
  • mock_auth_service (38-41)
tests/test_language_change.py (1)
  • mock_page (10-21)
tests/test_notification_bell.py (2)
  • mock_page (12-24)
  • test_notification_bell_opens_drawer (27-52)
src/switchcraft/gui_modern/app.py (3)
  • goto_tab (1100-1108)
  • ModernApp (25-1812)
  • _toggle_notification_drawer (140-216)
src/switchcraft/services/auth_service.py (3)
  • initiate_device_flow (27-44)
  • poll_for_token (47-90)
  • save_token (93-95)
src/switchcraft/gui_modern/views/settings_view.py (2)
  • ModernSettingsView (17-1802)
  • _build_general_tab (96-167)
src/switchcraft/gui_modern/views/script_upload_view.py (1)
  • on_change (54-62)
tests/test_all_buttons.py (3)
tests/test_button_functionality.py (1)
  • mock_page (11-26)
tests/test_github_login.py (2)
  • mock_page (13-34)
  • run_task (22-23)
tests/test_github_login_real.py (2)
  • mock_page (12-34)
  • run_task (22-23)
tests/test_all_three_issues.py (3)
src/switchcraft/gui_modern/app.py (1)
  • _toggle_notification_drawer (140-216)
src/switchcraft/gui_modern/views/settings_view.py (3)
  • _start_github_login (667-830)
  • _build_general_tab (96-167)
  • _build_cloud_sync_section (169-182)
src/switchcraft/services/notification_service.py (1)
  • get_notifications (115-116)
tests/test_winget_details.py (1)
src/switchcraft/gui_modern/views/winget_view.py (3)
  • ModernWingetView (13-1053)
  • _show_details_ui (465-767)
  • _load_details (335-463)
tests/test_button_functionality.py (3)
src/switchcraft/gui_modern/views/home_view.py (1)
  • ModernHomeView (6-156)
src/switchcraft/gui_modern/views/category_view.py (1)
  • CategoryView (41-116)
src/switchcraft/gui_modern/views/winget_view.py (1)
  • ModernWingetView (13-1053)
tests/test_github_login_real.py (2)
tests/test_github_login.py (4)
  • mock_page (13-34)
  • run_task (22-23)
  • mock_open (27-31)
  • mock_auth_service (38-41)
src/switchcraft/services/auth_service.py (2)
  • initiate_device_flow (27-44)
  • poll_for_token (47-90)
src/switchcraft/gui_modern/views/analyzer_view.py (1)
src/switchcraft/utils/i18n.py (1)
  • get (179-206)
tests/test_intune_store_search.py (2)
src/switchcraft/gui_modern/views/intune_store_view.py (4)
  • _get_token (119-125)
  • _show_error (234-247)
  • _run_search (127-232)
  • _update_list (249-292)
src/switchcraft/services/intune_service.py (2)
  • search_apps (404-464)
  • list_apps (372-402)
src/switchcraft/gui_modern/views/intune_store_view.py (2)
src/switchcraft/utils/i18n.py (1)
  • get (179-206)
src/switchcraft/gui_modern/views/intune_view.py (1)
  • _update_ui (540-542)
🪛 actionlint (1.7.10)
.github/workflows/test.yml

15-15: context "matrix" is not allowed here. available contexts are "github", "inputs", "needs", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)


16-16: context "matrix" is not allowed here. available contexts are "github", "inputs", "needs", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)

.github/workflows/ci.yml

29-29: context "matrix" is not allowed here. available contexts are "github", "inputs", "needs", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)


30-30: context "matrix" is not allowed here. available contexts are "github", "inputs", "needs", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)

🪛 LanguageTool
docs/Intune_Configuration_Guide.md

[style] ~3-~3: Try moving the adverb to make the sentence clearer.
Context: ...uration Guide This guide describes how to correctly configure SwitchCraft policies using Microsoft Intune Custom Profiles ...

(SPLIT_INFINITIVE)

🔇 Additional comments (63)
.github/workflows/test.yml (2)

38-44: LGTM!

Adding explicit CI and GITHUB_ACTIONS environment variables is a reasonable practice for tests that may check these values, even though GitHub Actions typically sets them automatically.


52-55: Python 3.14 is stable and well-supported; however, Python 3.15 is currently alpha-only.

actions/setup-python@v6 supports Python 3.14 (stable versions available: 3.14.2, 3.14.1, 3.14.0) and aligns with the test-backend matrix. However, Python 3.15 is only available as pre-release alpha versions (3.15.0-alpha.5 through 3.15.0-alpha.1). The test-backend job's conditional logic (limiting 3.15 runs to main branch pushes) appears to account for this instability. If using 3.15 alpha is intentional, ensure CI expectations account for potential breaking changes in future releases.

docs/.vitepress/config.mts (1)

152-154: The configuration cors: false is appropriate for a VitePress documentation site in local development. While it is technically more restrictive than cors: true, Vite's security guidance recommends against using cors: true due to the security risk of allowing any origin. For a documentation site that doesn't require cross-origin requests during local development, cors: false is the correct choice.

Likely an incorrect or invalid review comment.

docs/Intune_Configuration_Guide.md (3)

1-16: Excellent error guidance and clear introduction!

The prominent warning about error -2016281112 and the explanation of the Data Type confusion is extremely valuable. This will save administrators significant troubleshooting time. The distinction between selecting "String" in the Intune Portal while providing XML content as the value is clearly articulated.

Note: The static analysis hint about "split infinitive" on line 3 is a false positive—"how to correctly configure" is perfectly acceptable and idiomatic English.


17-23: LGTM! Clear prerequisite documentation.

The ADMX ingestion prerequisite is clearly documented with the necessary OMA-URI path and data type.


24-202: Excellent structure and consistency across all policy settings!

The policy documentation is well-organized and consistently formatted:

  • Clear hierarchical structure by category
  • All XML snippets follow the same pattern (<enabled/>, <disabled/>, or <enabled/> with <data> elements)
  • Appropriate use of placeholders for sensitive data (API keys, client secrets, GUIDs, thumbprints)
  • Valid values documented for dropdown fields (Theme, UpdateChannel, AIProvider)
  • Each policy includes OMA-URI path, data type, and XML examples

The examples make it easy to copy and adapt for actual deployments.

.github/workflows/release.yml (1)

151-179: LGTM: fallback propagation during the dev bump looks consistent.

scripts/build_release.ps1 (4)

155-174: LGTM: clearer fallback defaults and warnings.


228-231: No review comment (whitespace-only change).


234-255: Nice extraction of Inno Setup discovery into a helper.


354-356: Good reuse of Get-InnoSetupPath for both installer flows.

Also applies to: 406-408

.github/workflows/docs_preview.yml (1)

48-55: LGTM — preview base path configured.

src/switchcraft/gui_modern/views/analyzer_view.py (1)

492-496: LGTM — SFX notice layout tweak looks good.

src/switchcraft/gui_modern/views/stack_manager_view.py (1)

41-55: LGTM — description row expansion should prevent clipping.

src/switchcraft/gui_modern/views/wingetcreate_view.py (1)

100-118: LGTM — manifest info row expansion looks good.

.github/workflows/ci.yml (2)

3-17: LGTM — workflow triggers re-enabled with docs/markdown ignores.


55-56: LGTM — explicit CI flags set for tests.

src/switchcraft/gui_modern/views/winget_view.py (3)

243-245: Good defensive handling for None results.

Normalizing None to an empty list before calling _show_list prevents downstream errors. This aligns well with the guard added in _show_list.


277-281: LGTM - Defensive None guard.

This guard ensures stable behavior when results is None, preventing TypeError on len(results) and during filtering.


694-732: Robust UI refresh pattern with proper sequencing.

Creating a new Column instance and re-assigning both details_area and right_pane.content is the correct approach to force Flet to recognize changes. The sequential update order (child → parent → view → page) is appropriate.

The extensive exception handling with debug logging is good for diagnosing issues without crashing.

src/switchcraft/services/intune_service.py (2)

394-402: Good: Explicit re-raising of network exceptions enables proper error handling upstream.

Re-raising Timeout and RequestException separately allows callers to handle these cases distinctly (e.g., show timeout-specific UI messages).


445-464: Correct early propagation of network errors prevents unnecessary fallback attempts.

Re-raising Timeout and RequestException immediately is the right approach—there's no point falling back to client-side filtering if the network is unavailable.

tests/test_winget_details.py (2)

143-154: Good recursive error text collection pattern.

The collect_text helper effectively traverses the control tree to find error messages. The assertion correctly checks for error indicators.


24-32: The mock chain is correctly configured and will properly intercept WingetHelper. The fixture matches the actual access pattern in winget_view.py:

  • AddonService()mock_addon.return_value
  • .import_addon_module(...)mock_addon_instance.import_addon_module.return_value
  • .WingetHelper()mock_addon_instance.import_addon_module.return_value.WingetHelper.return_value

The code accesses these attributes in the exact order the mock chain assumes, so the mock will be applied correctly.

Likely an incorrect or invalid review comment.

tests/test_all_buttons.py (4)

14-28: Good recursive button discovery helper.

The function correctly handles multiple container types (controls, content, actions). Type checking for various button types is thorough.


30-47: Previous review feedback addressed: Test coupling fixed.

The shared logic is now extracted into collect_all_buttons() helper function, and test_all_views_have_buttons() no longer returns a value. This addresses the coupling issue from the previous review.

Also applies to: 49-126


147-153: Improved: Now asserts on buttons without handlers instead of always passing.

This is a significant improvement over the previous assert True. The test now properly fails if buttons lack handlers.


245-247: Previous review feedback addressed: Meaningful assertions added.

The weak assert len(buttons) >= 0 has been replaced with proper type checking and a requirement for at least one button. Good improvement.

tests/test_all_three_issues.py (4)

14-47: Comprehensive mock_page fixture covers all necessary attributes.

The fixture properly mocks dialog, end_drawer, run_task, and open behaviors. The mock_open helper correctly handles both AlertDialog and NavigationDrawer types.


77-79: Good practice: CI environment skip for time-dependent tests.

Skipping tests with time.sleep() in CI environments prevents flaky failures. Consider also using @pytest.mark.slow to allow selective execution.

Also applies to: 128-130


107-118: Recursive dropdown finder is robust.

The find_dropdown function correctly traverses nested controls and checks for language-related labels. This handles various UI structures.


195-220: Existence tests provide good baseline coverage.

These tests verify that critical UI elements exist and have handlers. They're fast and don't require time.sleep(), making them reliable for CI.

src/switchcraft/gui_modern/views/intune_store_view.py (1)

185-200: Timeout completion guard looks solid.

The additional timeout logging and default-error fallback make the background search behavior deterministic.

tests/test_ui_interactions_critical.py (6)

14-47: Mock page fixture covers the required UI hooks.


51-67: AuthService patching fixture looks good.


143-170: Drawer interaction test reads well.


173-193: Good guardrail around language dropdown wiring.


195-209: Login button existence test is clear and focused.


211-220: Notification button wiring check looks good.

tests/test_github_login_real.py (4)

11-34: Mock page fixture is well-scoped for the tests.


37-54: AuthService fixture setup is solid.


82-100: Language change refresh assertion is straightforward.


103-121: Notification drawer test is clear and consistent.

src/switchcraft/gui_modern/app.py (6)

165-199: Drawer open/close hardening is solid.


278-305: Open sequence and post-open verification look good.


815-818: Loading-screen sequencing comment is helpful.


1008-1009: Post-add update ensures the layout renders immediately.


1225-1231: Extra drawer-open enforcement is a good safeguard.


1823-1824: Explicit app exposure on the page is clear.

tests/test_button_functionality.py (2)

10-26: Mock page fixture is comprehensive for UI tests.


30-38: Recursive clickable discovery helper is clear.

tests/test_intune_store_search.py (4)

13-23: LGTM - Mock page fixture is well-structured.

The fixture correctly mocks ft.Page with run_task executing immediately for synchronous testing.


26-32: LGTM - IntuneService mock fixture is appropriate.

The context manager properly patches at the import location and yields the instance.


91-115: LGTM - Network error test is well-structured.

The test correctly mocks a RequestException, triggers search, and asserts error handling.


118-147: LGTM - Successful results test validates the happy path.

The test properly mocks search results and verifies they propagate to _update_list.

src/switchcraft/gui_modern/views/settings_view.py (8)

123-127: LGTM - Improved handler binding pattern.

Using a named function _handle_lang_change with a guard check for e.control.value is cleaner and safer than a direct lambda assignment.


667-708: Good defensive pattern for GitHub login initialization.

The flow correctly shows a loading dialog immediately, then runs network operations in background. Error handling properly closes the loading dialog before showing error snacks.

One minor observation: The _init_flow function is called from a background thread (_flow_complete), but it also calls self.app_page.update() on lines 697 and 705. While this appears to work in practice with Flet, it's technically a UI update from a background thread.

Consider wrapping UI updates in _init_flow with run_task for consistency:

def _update_on_error():
    loading_dlg.open = False
    self.app_page.update()
    self._show_snack("Login init failed", "RED")

if hasattr(self.app_page, 'run_task'):
    self.app_page.run_task(_update_on_error)
else:
    _update_on_error()

730-738: Good UX improvement - Selectable text fields.

Making the verification URL and user code selectable (lines 734, 736) improves usability for manual copy operations.


741-778: Comprehensive dialog presentation fallbacks.

The multiple fallback paths (page.open → manual assignment → secondary fallback) ensure the dialog opens across different Flet versions and configurations. The logging statements aid debugging.


780-800: Token polling correctly uses background thread with main-thread UI update.

The _poll_token runs in a daemon thread and properly uses run_task to marshal UI updates back to the main thread.


802-830: Flow completion handles threading correctly with appropriate fallbacks.

The orchestration properly starts the flow in a background thread and uses run_task to show the dialog on the main thread. Multiple fallback paths handle edge cases.


998-1008: Good addition of logging for language change debugging.

The debug/info log statements help trace language change flow issues.


1056-1105: Language reload follows consistent pattern with appropriate fallbacks.

The _reload_app function and its invocation via run_task with fallbacks mirrors the GitHub login pattern, ensuring main-thread safety for UI updates. All error paths consistently show a success/fallback message to the user.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@FaserF FaserF added the no-auto-merge Exclude a PR from the auto-merge workflow label Jan 19, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@src/switchcraft/services/intune_service.py`:
- Around line 484-489: The exception handling in the get_app_details block is
inconsistent: it creates a new Timeout losing the original trace and wraps other
request errors in a generic Exception; change it to preserve original exceptions
and stack traces (match the list_apps pattern) by logging the error and
re-raising the original exception or re-raising a new exception using "raise ...
from e" for requests.exceptions.RequestException, and for
requests.exceptions.Timeout either re-raise the caught Timeout or raise a new
Timeout using "from" to retain cause; update the handlers around
requests.exceptions.Timeout and requests.exceptions.RequestException
(referencing logger, app_id, requests.exceptions.Timeout, and
requests.exceptions.RequestException) so both preserve the original exception
context consistently.
- Around line 394-399: In the except block inside list_apps where you catch
requests.exceptions.Timeout, change the handler to capture the original
exception (e.g., except requests.exceptions.Timeout as e:), log the timeout, and
preserve the original stack/chain by either using a bare raise or re-raising a
new exception using "from e" (for example raise TimeoutError("Request timed
out...") from e) instead of creating a new Timeout without chaining; this
matches the RequestException handling and preserves the original traceback for
debugging.

In `@tests/test_all_three_issues.py`:
- Around line 51-63: The mock_auth_service fixture currently returns the mock
inside the with patch(...) context, which closes the patch before tests run;
change mock_auth_service to yield the mock instead of return (i.e., use "with
patch('switchcraft.gui_modern.views.settings_view.AuthService') as mock: ...;
yield mock") so the patch remains active for the duration of the test and the
mocked methods (initiate_device_flow, poll_for_token, save_token) are used.

In `@tests/test_button_functionality.py`:
- Around line 141-210: The tests test_winget_view_search_button and
test_intune_store_search_button currently swallow all exceptions with bare
"except Exception: pass", which hides real regressions; replace those bare
except blocks (the one around search_button.on_click invocation and the one
around view.btn_search.on_click) with explicit failure handling—either call
pytest.fail(...) with the exception message or append the exception to a local
failures list and assert at the end—so any unexpected exception during the click
handler (on_click) on search_button or view.btn_search will cause the test to
fail and surface the error.

In `@tests/test_github_login_real.py`:
- Around line 58-75: The fixture mock_auth_service currently returns from inside
the with patch('switchcraft.gui_modern.views.settings_view.AuthService') context
so the patch is torn down before tests run; change the fixture to yield the
mocked object instead of returning (i.e., replace the final "return mock" with
"yield mock") so the patch remains active for the test duration, keeping the
patched AuthService methods (initiate_device_flow, poll_for_token, save_token)
available; ensure the with patch(...) context is used so it automatically
unpatches after the yield.

In `@tests/test_ui_interactions_critical.py`:
- Around line 71-88: The fixture mock_auth_service currently returns inside the
with patch('switchcraft.gui_modern.views.settings_view.AuthService') context
which ends the patch too early; change the fixture to yield the mock instead
(use "with patch(...) as mock:" then "yield mock") so the patch remains active
during tests, and ensure the mocked methods (initiate_device_flow,
poll_for_token, save_token) are assigned on the yielded mock_service so tests
use those mocked responses.
♻️ Duplicate comments (3)
.github/workflows/ci.yml (2)

28-43: LGTM! Previous review concerns addressed correctly.

The job-level if referencing matrix has been removed, and conditions are now properly placed at the step level where matrix context is available. The allow-prereleases flag correctly enables Python 3.15 prerelease support.


46-93: Same DRY opportunity as in test.yml.

The version-gating condition is repeated across all steps. Consider extracting it to reduce duplication, as noted for test.yml.

tests/test_ui_interactions_critical.py (1)

200-219: Dropdown lookup should be recursive.

This test only checks direct children, so it can miss a nested language dropdown. Use a recursive search like the one in your other test.

🔧 Suggested fix
-    lang_dd = None
-    for control in general_tab.controls:
-        if isinstance(control, ft.Dropdown):
-            lang_dd = control
-            break
+    def find_dropdown(control):
+        if isinstance(control, ft.Dropdown):
+            return control
+        if hasattr(control, 'controls'):
+            for child in control.controls:
+                result = find_dropdown(child)
+                if result:
+                    return result
+        if hasattr(control, 'content'):
+            return find_dropdown(control.content)
+        return None
+    lang_dd = find_dropdown(general_tab)
🧹 Nitpick comments (5)
.github/workflows/test.yml (1)

33-49: Consider extracting the repeated condition into a reusable pattern.

The same condition (matrix.python-version == '3.14' || (matrix.python-version == '3.15' && github.event_name == 'push' && github.ref == 'refs/heads/main')) is repeated across multiple steps. While this works, you could consider using a job output or env variable to reduce duplication.

♻️ Optional: Use an env variable for the condition
    env:
      SHOULD_RUN: ${{ matrix.python-version == '3.14' || (matrix.python-version == '3.15' && github.event_name == 'push' && github.ref == 'refs/heads/main') }}
    steps:
      # ...
      - name: Install dependencies
        if: env.SHOULD_RUN == 'true'
        run: |
          # ...
src/switchcraft/gui_modern/views/intune_store_view.py (1)

188-193: Redundant import: time is already imported at file top.

The import time on line 189 is unnecessary since time is already imported at line 4.

🧹 Remove redundant import
-            # Wait a bit more to ensure result_holder is set (max 1 second)
-            import time
+            # Wait a bit more to ensure result_holder is set (max 1 second)
             timeout_count = 0
scripts/build_release.ps1 (1)

155-173: Prefer an env override for the fallback version.

Hard‑coding the fallback version can drift from release tooling. Consider reading an env override first and derive the numeric variant from it.

♻️ Suggested tweak
-# Fallback version if extraction fails (should match current release cycle)
-$AppVersion = "2026.1.2"
-$AppVersionNumeric = "2026.1.2"
+# Fallback version if extraction fails (should match current release cycle)
+$FallbackVersion = if ($env:FALLBACK_VERSION) { $env:FALLBACK_VERSION } else { "2026.1.2" }
+$AppVersion = $FallbackVersion
+$AppVersionNumeric = $FallbackVersion -replace '-.*', ''
tests/test_intune_store_search.py (1)

64-107: Avoid real 65–70s sleeps in background threads.

Even with timeout simulation, the daemon thread still sleeps for a long time. Consider using a synchronization primitive (e.g., an event wait) or a stub that blocks without long sleeps to keep tests fast and avoid lingering threads.

Also applies to: 177-251

src/switchcraft/gui_modern/views/winget_view.py (1)

243-245: Route _show_list through _run_ui_update for thread safety.
_search() runs in a background thread; if Flet requires UI updates on the main thread, this call should be marshaled.

♻️ Suggested change
-                data = result_holder["data"] if result_holder["data"] is not None else []
-                self._show_list(data, filter_by, query)
+                data = result_holder["data"] if result_holder["data"] is not None else []
+                self._run_ui_update(lambda: self._show_list(data, filter_by, query))

Please confirm Flet’s guidance on UI updates from background threads to ensure this aligns with best practices.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/release.yml (1)

148-159: DEV_VERSION uses non-PEP 440 format.

The workflow sets DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-dev" (line 154), but the generate_release_version.py script produces PEP 440-compliant versions like X.Y.Z.dev0+sha. The -dev suffix is not PEP 440 compliant and may cause validation issues.

🔧 Suggested fix for PEP 440 compliance
-          DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-dev"
+          DEV_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev0"
🧹 Nitpick comments (2)
.github/scripts/generate_release_version.py (1)

83-86: Consider anchoring the validation regex for consistency.

The regex r'^\d+\.\d+\.\d+' only checks the prefix, so it would accept malformed versions like 1.2.3.4.5 or 1.2.3foo. The workflow file uses a stricter anchored pattern. For consistency:

♻️ Suggested stricter validation
-        if not next_version or not re.match(r'^\d+\.\d+\.\d+', next_version):
+        if not next_version or not re.match(r'^\d+\.\d+\.\d+(\.[a-z]+\d+|[a-z]+\d+)?(\+[a-zA-Z0-9.-]+)?$', next_version):
pyproject.toml (1)

17-32: Duplicate requests dependency.

The requests package appears twice in the dependencies list (lines 20 and 31). While this won't cause runtime issues, it should be cleaned up.

♻️ Remove duplicate
     "defusedxml",
     "PyJWT",
-    "winotify",
-    "requests"
+    "winotify"
 ]

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/build_release.ps1 (1)

155-174: Version suffix stripping may not handle PEP 440 .dev0 format.

Line 157 uses -replace '-.*', '' to strip suffixes, but PEP 440 development versions use .dev0 (dot, not hyphen). For version 2026.1.2.dev0+9d07a00, this regex won't strip the suffix.

🔧 Suggested fix
 $AppVersion = if ($env:SWITCHCRAFT_VERSION) { $env:SWITCHCRAFT_VERSION } else { "2026.1.2" }
-$AppVersionNumeric = $AppVersion -replace '-.*', '' # Remove suffixes like -dev for numeric parsing
+# Remove PEP 440 suffixes: .dev0, .a1, .b1, .rc1, +build, or legacy -dev
+$AppVersionNumeric = $AppVersion -replace '(\.dev|\.a|\.b|\.rc|\+|-)[0-9].*$', ''
🤖 Fix all issues with AI agents
In @.github/workflows/release.yml:
- Around line 181-188: The sed replacements target a simple assignment but the
script uses a conditional assignment for $AppVersion and $AppVersionNumeric
(e.g. $AppVersion = if (...) { ... } else { "2026.1.2" }), so update the
workflow to either (A) change the sed regex to match the else-block pattern and
replace the literal inside the else { "..." } for both $AppVersion and
$AppVersionNumeric, or (B) modify the script to expose a single fallback
variable (e.g. $AppVersionFallback) that is used in the conditional and then
change the workflow to sed-replace that fallback variable (and keep the existing
replacement for FALLBACK_VERSION in generate_release_version.py); target the
symbols $AppVersion, $AppVersionNumeric, FALLBACK_VERSION and the sed commands
in the workflow when making the change.

In `@tests/test_github_login_real.py`:
- Around line 32-55: The mock_page fixture's mock_open only handles
ft.AlertDialog so page.end_drawer is never set, breaking tests expecting a
NavigationDrawer; update the mock_page fixture to initialize page.end_drawer =
None and extend mock_open (the function assigned to page.open) to also detect
ft.NavigationDrawer controls and set page.end_drawer = control and control.open
= True before calling page.update(), ensuring tests interacting with
NavigationDrawer behave like real ft.Page.

In `@tests/test_ui_interactions_critical.py`:
- Around line 129-142: The current find_dropdown function is too restrictive by
matching label text containing "Language" and misses translated labels; update
find_dropdown (used to set lang_dd) to mirror the simpler approach from
test_language_dropdown_handler_exists: return the first instance of ft.Dropdown
found under general_tab (i.e., remove the label substring check), or if you must
disambiguate multiple dropdowns, inspect the Dropdown.options values for known
language keys/values instead of relying on label text; ensure lang_dd assignment
uses this updated find_dropdown so the assertion can locate the language
dropdown.
♻️ Duplicate comments (1)
src/switchcraft/services/intune_service.py (1)

487-489: Creating a new RequestException loses call site information.

Line 489 creates a new RequestException with a modified message, which discards the original exception's type-specific information (e.g., ConnectionError, HTTPError). Unlike the Timeout handling above it, this approach loses specificity. Consider using bare raise for consistency with list_apps.

🔧 Suggested fix
         except requests.exceptions.RequestException as e:
             logger.error(f"Network error getting app details: {e}")
-            raise requests.exceptions.RequestException(f"Network error: {str(e)}") from e
+            raise
🧹 Nitpick comments (1)
tests/test_all_three_issues.py (1)

66-137: Optional: replace fixed sleeps with polling to reduce flakiness.
The tests already use async flows; a small poll_until helper would make timing more robust.

Comment on lines +181 to +188
# Update fallback versions in build scripts and version generator
# Update build_release.ps1 fallback
sed -i "s/\$AppVersion = \".*\"/\$AppVersion = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1
sed -i "s/\$AppVersionNumeric = \".*\"/\$AppVersionNumeric = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1
# Update generate_release_version.py fallback
sed -i "s/FALLBACK_VERSION = \".*\"/FALLBACK_VERSION = \"$FALLBACK_VERSION\"/" .github/scripts/generate_release_version.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current format of AppVersion assignment in build_release.ps1
echo "=== Checking AppVersion assignments ===" 
grep -n 'AppVersion' scripts/build_release.ps1 | head -20
echo ""
echo "=== Checking context around line 156 ==="
sed -n '150,165p' scripts/build_release.ps1

Repository: FaserF/SwitchCraft

Length of output: 2092


The sed pattern will not match the conditional assignment syntax in build_release.ps1.

Line 156 of scripts/build_release.ps1 uses a conditional:

$AppVersion = if ($env:SWITCHCRAFT_VERSION) { $env:SWITCHCRAFT_VERSION } else { "2026.1.2" }

The sed pattern s/\$AppVersion = \".*\"/\$AppVersion = \"$FALLBACK_VERSION\"/ expects $AppVersion = "..." but will not match this if-else syntax. The command will silently fail, leaving the fallback version unchanged.

🔧 Suggested fix

Target the fallback value inside the else block:

-          sed -i "s/\$AppVersion = \".*\"/\$AppVersion = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1
-          sed -i "s/\$AppVersionNumeric = \".*\"/\$AppVersionNumeric = \"$FALLBACK_VERSION\"/" scripts/build_release.ps1
+          # Update the fallback version in the else block
+          sed -i "s/else { \"[0-9]*\.[0-9]*\.[0-9]*\" }/else { \"$FALLBACK_VERSION\" }/" scripts/build_release.ps1

Alternatively, consider extracting the fallback to a dedicated variable at the top of the script that's easier to update with sed.

🤖 Prompt for AI Agents
In @.github/workflows/release.yml around lines 181 - 188, The sed replacements
target a simple assignment but the script uses a conditional assignment for
$AppVersion and $AppVersionNumeric (e.g. $AppVersion = if (...) { ... } else {
"2026.1.2" }), so update the workflow to either (A) change the sed regex to
match the else-block pattern and replace the literal inside the else { "..." }
for both $AppVersion and $AppVersionNumeric, or (B) modify the script to expose
a single fallback variable (e.g. $AppVersionFallback) that is used in the
conditional and then change the workflow to sed-replace that fallback variable
(and keep the existing replacement for FALLBACK_VERSION in
generate_release_version.py); target the symbols $AppVersion,
$AppVersionNumeric, FALLBACK_VERSION and the sed commands in the workflow when
making the change.

Comment on lines 32 to 55
@pytest.fixture
def mock_page():
"""Create a mock Flet page."""
page = MagicMock(spec=ft.Page)
page.dialog = None
page.update = MagicMock()
page.snack_bar = MagicMock(spec=ft.SnackBar)
page.snack_bar.open = False
page.switchcraft_app = MagicMock()

# Mock run_task to actually execute the function
def run_task(func):
func()
page.run_task = run_task

# Mock page.open to set dialog and open it
def mock_open(control):
if isinstance(control, ft.AlertDialog):
page.dialog = control
control.open = True
page.update()
page.open = mock_open

return page
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mock page doesn’t handle NavigationDrawer, causing drawer test failure.
mock_open only handles AlertDialog, so mock_page.end_drawer never gets set. The CI failure (“Drawer should be created but end_drawer is None”) is consistent with this. Add end_drawer initialization and handle NavigationDrawer in mock_open.

🛠️ Proposed fix
 def mock_page():
     """Create a mock Flet page."""
     page = MagicMock(spec=ft.Page)
     page.dialog = None
+    page.end_drawer = None
     page.update = MagicMock()
     page.snack_bar = MagicMock(spec=ft.SnackBar)
     page.snack_bar.open = False
     page.switchcraft_app = MagicMock()
@@
     def mock_open(control):
         if isinstance(control, ft.AlertDialog):
             page.dialog = control
             control.open = True
+        elif isinstance(control, ft.NavigationDrawer):
+            page.end_drawer = control
+            control.open = True
         page.update()
     page.open = mock_open
🤖 Prompt for AI Agents
In `@tests/test_github_login_real.py` around lines 32 - 55, The mock_page
fixture's mock_open only handles ft.AlertDialog so page.end_drawer is never set,
breaking tests expecting a NavigationDrawer; update the mock_page fixture to
initialize page.end_drawer = None and extend mock_open (the function assigned to
page.open) to also detect ft.NavigationDrawer controls and set page.end_drawer =
control and control.open = True before calling page.update(), ensuring tests
interacting with NavigationDrawer behave like real ft.Page.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/test_github_login_real.py`:
- Around line 125-145: The test fails because the mock_page fixture doesn't
initialize or create mock_page.end_drawer (and mock_open doesn't simulate
ft.NavigationDrawer), so when _toggle_notification_drawer runs the test's
assertions about mock_page.end_drawer and its .open state fail; fix by updating
the mock_page fixture (or the test setup) to create and assign a
NavigationDrawer-like object to mock_page.end_drawer when the drawer is opened
(or have mock_open return/instantiate an ft.NavigationDrawer substitute), ensure
mock_page.update is a callable MagicMock, and keep _toggle_notification_drawer
invoked as-is so the test can assert the drawer exists, is an instance of
NavigationDrawer, and is open.
♻️ Duplicate comments (1)
tests/test_github_login_real.py (1)

32-55: Pipeline failure: mock_page fixture missing NavigationDrawer handling.

The pipeline fails at line 142 because mock_page.end_drawer is never set. The mock_open function only handles ft.AlertDialog but not ft.NavigationDrawer. This was flagged in a previous review but appears not fully addressed in this file (though it was fixed in test_ui_interactions_critical.py).

🐛 Fix to handle NavigationDrawer
 `@pytest.fixture`
 def mock_page():
     """Create a mock Flet page."""
     page = MagicMock(spec=ft.Page)
     page.dialog = None
+    page.end_drawer = None
     page.update = MagicMock()
     page.snack_bar = MagicMock(spec=ft.SnackBar)
     page.snack_bar.open = False
     page.switchcraft_app = MagicMock()

     # Mock run_task to actually execute the function
     def run_task(func):
         func()
     page.run_task = run_task

     # Mock page.open to set dialog and open it
     def mock_open(control):
         if isinstance(control, ft.AlertDialog):
             page.dialog = control
             control.open = True
+        elif isinstance(control, ft.NavigationDrawer):
+            page.end_drawer = control
+            control.open = True
         page.update()
     page.open = mock_open

     return page
🧹 Nitpick comments (3)
tests/test_settings_language.py (1)

31-36: Consider setting run_task in setUp for consistency.

The run_task mock is set after ModernSettingsView initialization. While this works here since _on_lang_change is the method under test, other test files (e.g., test_ui_interactions_critical.py, test_github_login_real.py) set run_task in the fixture/setUp before view creation. For consistency and to avoid potential issues if the view's __init__ ever uses run_task, consider moving this to setUp.

Suggested change
     def setUp(self):
         ...
         self.page.show_snack_bar = MagicMock()
+        # Ensure run_task is available for UI updates
+        self.page.run_task = lambda func: func()

     `@patch`('switchcraft.utils.config.SwitchCraftConfig.set_user_preference')
     `@patch`('switchcraft.utils.i18n.i18n.set_language')
     def test_language_change_immediate(self, mock_set_language, mock_set_pref):
         """Test that language change is applied immediately."""
         from switchcraft.gui_modern.views.settings_view import ModernSettingsView

         view = ModernSettingsView(self.page)
-        # Ensure run_task is available for UI updates
-        self.page.run_task = lambda func: func()

         # Simulate language change
         view._on_lang_change("en")
tests/test_ui_interactions_critical.py (2)

15-33: poll_until helper is duplicated across test files.

This utility function is duplicated verbatim in test_github_login_real.py. Consider extracting it to a shared location like tests/conftest.py or a tests/helpers.py module.


35-68: mock_page fixture is duplicated across test files.

This fixture is nearly identical to the one in test_github_login_real.py. Consider moving shared fixtures to tests/conftest.py for reuse across test modules.

Comment on lines 125 to 145
def test_notification_bell_opens_drawer(mock_page):
"""Test that notification bell actually opens the drawer."""
from switchcraft.gui_modern.app import ModernApp

app = ModernApp(mock_page)
# Ensure run_task is available for UI updates
mock_page.run_task = lambda func: func()

# Mock notification service
with patch.object(app, 'notification_service') as mock_notif:
mock_notif.get_notifications.return_value = []

# Simulate button click
mock_event = MagicMock()
app._toggle_notification_drawer(mock_event)

# Check that drawer was created and opened
assert mock_page.end_drawer is not None, "Drawer should be created"
assert isinstance(mock_page.end_drawer, ft.NavigationDrawer), "Drawer should be NavigationDrawer"
assert mock_page.end_drawer.open is True, "Drawer should be open"
assert mock_page.update.called, "Page should be updated"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Test will fail due to missing end_drawer initialization.

This test expects mock_page.end_drawer to be set after calling _toggle_notification_drawer, but the mock_page fixture doesn't initialize end_drawer or handle NavigationDrawer in mock_open. Once the fixture is fixed as suggested above, this test should pass.

🧰 Tools
🪛 GitHub Actions: CI

[error] 142-142: test_notification_bell_opens_drawer failed: Drawer should be created when notification bell is clicked.

🤖 Prompt for AI Agents
In `@tests/test_github_login_real.py` around lines 125 - 145, The test fails
because the mock_page fixture doesn't initialize or create mock_page.end_drawer
(and mock_open doesn't simulate ft.NavigationDrawer), so when
_toggle_notification_drawer runs the test's assertions about
mock_page.end_drawer and its .open state fail; fix by updating the mock_page
fixture (or the test setup) to create and assign a NavigationDrawer-like object
to mock_page.end_drawer when the drawer is opened (or have mock_open
return/instantiate an ft.NavigationDrawer substitute), ensure mock_page.update
is a callable MagicMock, and keep _toggle_notification_drawer invoked as-is so
the test can assert the drawer exists, is an instance of NavigationDrawer, and
is open.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@tests/conftest.py`:
- Around line 25-36: The docstring for skip_if_ci is misleading: update the
docstring for the skip_if_ci function (which calls is_ci_environment() and
pytest.skip) to remove the "Returns" section and instead state that the function
performs an immediate skip of the test when running in CI; keep a short
description, Args section for reason, and a brief behavior note that it calls
pytest.skip() immediately if is_ci_environment() is True.

In `@tests/test_github_login.py`:
- Around line 186-194: The current mock_thread calls the target immediately and
then returns a real threading.Thread (original_thread) so the target runs again
when thread.start() is called; replace this with a Thread-like mock class (e.g.,
ImmediateThread) that accepts the same ctor args (target, daemon, kwargs),
stores the target but does not set it on an underlying real thread, and
implements start() to invoke the stored target exactly once; update the test to
assign threading.Thread = ImmediateThread (and restore it after) instead of
returning original_thread instances so target is executed only once.

In `@tests/test_intune_store_search.py`:
- Around line 275-278: The assertion about the search not completing should be
done before releasing the worker; move the check of search_completed.is_set() to
occur prior to calling search_blocker.set() so the test cannot race with the
worker thread; update the snippet around search_blocker and search_completed
(the two symbols to change) so you assert not search_completed.is_set() first,
then call search_blocker.set() to allow the thread to exit.
♻️ Duplicate comments (1)
tests/test_github_login_real.py (1)

82-104: Verify mock_page supports NavigationDrawer/end_drawer.

This test assumes page.open(...) sets end_drawer when a NavigationDrawer is opened. Please confirm the shared mock_page fixture in tests/conftest.py implements that; otherwise these assertions will fail.

#!/bin/bash
# Inspect mock_page fixture for NavigationDrawer/end_drawer handling
fd -t f 'conftest.py'
rg -n "def mock_page" -C3 $(fd -t f 'conftest.py')
rg -n "end_drawer|NavigationDrawer|page\.open" -C3 $(fd -t f 'conftest.py')
🧹 Nitpick comments (5)
tests/conftest.py (1)

95-104: Consider handling SnackBar in mock_page.open.

Your UI code uses page.open(page.snack_bar). The mock currently ignores SnackBar, so tests that assert snack-bar state can’t observe it.

♻️ Suggested tweak
             def mock_open(control):
                 if isinstance(control, ft.AlertDialog):
                     self.dialog = control
                     control.open = True
                 elif isinstance(control, ft.NavigationDrawer):
                     self.end_drawer = control
                     control.open = True
+                elif isinstance(control, ft.SnackBar):
+                    self.snack_bar = control
+                    control.open = True
                 self.update()
tests/test_template_company.py (1)

5-13: Align fallback CI detection with shared helper.

To avoid drift, mirror the conftest.is_ci_environment() conditions in the fallback.

♻️ Suggested tweak
     def is_ci_environment():
-        return os.environ.get('CI') == 'true' or os.environ.get('GITHUB_ACTIONS') == 'true'
+        return (
+            os.environ.get('CI') == 'true' or
+            os.environ.get('GITHUB_ACTIONS') == 'true' or
+            os.environ.get('GITHUB_RUN_ID') is not None
+        )
tests/test_github_login.py (1)

23-45: Consider reusing the shared mock_page fixture.

There’s already a richer mock_page in tests/conftest.py (end_drawer + open semantics). Reusing it avoids drift across UI tests.

♻️ Suggested refactor
-@pytest.fixture
-def mock_page():
-    """Create a mock Flet page with run_task support."""
-    page = MagicMock(spec=ft.Page)
-    page.dialog = None
-    page.update = MagicMock()
-    page.snack_bar = MagicMock(spec=ft.SnackBar)
-    page.snack_bar.open = False
-
-    # Mock run_task to actually execute the function
-    def run_task(func):
-        func()
-    page.run_task = run_task
-
-    # Mock page.open to set dialog and open it
-    def mock_open(control):
-        if isinstance(control, ft.AlertDialog):
-            page.dialog = control
-            control.open = True
-        page.update()
-    page.open = mock_open
-
-    return page
+from tests.conftest import mock_page
tests/test_intune_store_search.py (1)

24-55: Consider reusing shared poll_until/mock_page from tests.conftest.

This avoids helper drift and keeps test behavior consistent across modules.

tests/test_winget_details.py (1)

23-55: Consider reusing shared poll_until/mock_page from tests.conftest.

This keeps helper behavior consistent across the test suite.

Comment on lines 186 to 194
# Mock Thread to execute immediately
original_thread = threading.Thread
def mock_thread(target=None, daemon=False, **kwargs):
thread = original_thread(target=target, daemon=daemon, **kwargs)
# Execute immediately instead of starting thread
if target:
target()
return thread
threading.Thread = mock_thread
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Thread mock executes targets twice.

The mock calls target() immediately in the function body, then returns a real threading.Thread object that still has the original target. When the application code calls thread.start(), it executes the target a second time, causing double execution and potential test flakiness.

🔧 Use an ImmediateThread mock instead
     original_thread = threading.Thread
-    def mock_thread(target=None, daemon=False, **kwargs):
-        thread = original_thread(target=target, daemon=daemon, **kwargs)
-        # Execute immediately instead of starting thread
-        if target:
-            target()
-        return thread
-    threading.Thread = mock_thread
+    class ImmediateThread:
+        def __init__(self, target=None, daemon=False, **kwargs):
+            self._target = target
+            self.daemon = daemon
+        def start(self):
+            if self._target:
+                self._target()
+        def join(self, *args, **kwargs):
+            pass
+    threading.Thread = ImmediateThread
🤖 Prompt for AI Agents
In `@tests/test_github_login.py` around lines 186 - 194, The current mock_thread
calls the target immediately and then returns a real threading.Thread
(original_thread) so the target runs again when thread.start() is called;
replace this with a Thread-like mock class (e.g., ImmediateThread) that accepts
the same ctor args (target, daemon, kwargs), stores the target but does not set
it on an underlying real thread, and implements start() to invoke the stored
target exactly once; update the test to assign threading.Thread =
ImmediateThread (and restore it after) instead of returning original_thread
instances so target is executed only once.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tests/test_github_login.py`:
- Around line 183-207: The ImmediateThread mock drops thread arguments; update
the ImmediateThread class (its __init__ and start methods) to accept explicit
args and kwargs parameters, store them on self (e.g., self.args, self.kwargs)
and call the target with those parameters in start (self.target(*self.args,
**self.kwargs)); keep existing behavior for daemon, _started, join, and is_alive
to preserve test semantics.

In `@tests/test_winget_details.py`:
- Around line 117-135: Replace the fragile time.sleep gating by introducing a
deterministic synchronization primitive: have slow_get_details (assigned to
mock_winget_helper.get_package_details) wait on a threading.Event (or
asyncio.Event) that the test controls, then call view._load_details(short_info)
and assert the loading UI (view.details_area contains a ft.ProgressBar) before
setting the event to allow the mock to return and finish; locate the mock
function slow_get_details, the assignment
mock_winget_helper.get_package_details, and the ModernWingetView usage
(view.did_mount and view._load_details) to implement the event-based gate so the
test no longer relies on timing.
🧹 Nitpick comments (1)
tests/test_winget_details.py (1)

12-39: Consolidate fallback helpers to avoid drift.

The inline fallback duplicates logic from tests.conftest. Consider moving the fallback into a shared helper (or failing fast with a clearer message) so behavior doesn’t silently diverge between environments.

Comment on lines +183 to +207
# Mock Thread to execute immediately - use ImmediateThread class
class ImmediateThread:
"""Thread-like mock that executes target immediately when start() is called."""
def __init__(self, target=None, daemon=False, **kwargs):
self.target = target
self.daemon = daemon
self.kwargs = kwargs
self._started = False

def start(self):
"""Execute the stored target exactly once."""
if not self._started and self.target:
self._started = True
self.target()

def join(self, timeout=None):
"""No-op for compatibility."""
pass

def is_alive(self):
"""Return False since we execute immediately."""
return False

original_thread = threading.Thread
threading.Thread = ImmediateThread
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the test file
fd -a "test_github_login.py" -type f

Repository: FaserF/SwitchCraft

Length of output: 180


🏁 Script executed:

#!/bin/bash
# Find settings_view file
fd -a "settings_view.py" -type f

Repository: FaserF/SwitchCraft

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Search for threading.Thread usage across the codebase
rg -n "threading\.Thread\(" --max-count 20

Repository: FaserF/SwitchCraft

Length of output: 9956


🏁 Script executed:

#!/bin/bash
# Also check for Thread imports and usage patterns
rg -n "from threading import|import threading" --max-count 20

Repository: FaserF/SwitchCraft

Length of output: 2370


🏁 Script executed:

#!/bin/bash
# Read the test file
fd "test_github_login.py" -type f -exec cat -n {} \;

Repository: FaserF/SwitchCraft

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Look for thread creation with args in the test files
rg -n "threading\.Thread.*args" tests/

Repository: FaserF/SwitchCraft

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Find and read the test file
find . -name "test_github_login.py" -exec cat -n {} \;

Repository: FaserF/SwitchCraft

Length of output: 8989


🏁 Script executed:

#!/bin/bash
# Search for threading.Thread calls with args parameter
rg "threading\.Thread\(.*args" tests/

Repository: FaserF/SwitchCraft

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Search for _start_github_login in settings_view.py to see how it creates threads
find . -name "settings_view.py" -path "*/gui_modern/views/*" -exec grep -n "_start_github_login\|threading.Thread" {} +

Repository: FaserF/SwitchCraft

Length of output: 783


🏁 Script executed:

#!/bin/bash
# Check if the production code in settings_view creates any Thread with args
find . -name "settings_view.py" -path "*/gui_modern/views/*" -exec cat {} \; | head -900 | tail -200

Repository: FaserF/SwitchCraft

Length of output: 9571


🏁 Script executed:

#!/bin/bash
# Check if there are any thread creations with args parameter in the entire codebase
rg "Thread\(.*args\s*=" --max-count 10

Repository: FaserF/SwitchCraft

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Check the Python threading.Thread signature to confirm expected parameters
python3 << 'EOF'
import threading
import inspect
sig = inspect.signature(threading.Thread.__init__)
print("threading.Thread signature:")
print(sig)
print("\nParameters:")
for name, param in sig.parameters.items():
    print(f"  {name}: {param}")
EOF

Repository: FaserF/SwitchCraft

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Let me check if the test itself uses args in thread creation
grep -n "threading.Thread" tests/test_github_login.py -A 1 -B 1

Repository: FaserF/SwitchCraft

Length of output: 224


🏁 Script executed:

#!/bin/bash
# Verify the actual signatures expected by analyzing the two production cases that use args
find . -name "analyzer_view.py" -path "*/gui/views/*" -exec grep -n "def _analyze_thread" {} +
find . -name "app.py" -path "*/gui/*" -exec grep -n "def _download_and_analyze" {} +

Repository: FaserF/SwitchCraft

Length of output: 163


ImmediateThread mock must forward args/kwargs to target.

The production code calls threading.Thread(target=func, args=(value,)) in at least two places (analyzer_view.py and app.py). The current mock doesn't capture or forward these—it only stores target and daemon, dropping any args/kwargs passed to __init__. This can hide bugs when tests using this mock exercise code paths that rely on thread arguments.

Update the __init__ signature to explicitly accept args and kwargs parameters, store them, and forward them when calling self.target() in start():

Proposed fix
 class ImmediateThread:
     """Thread-like mock that executes target immediately when start() is called."""
-    def __init__(self, target=None, daemon=False, **kwargs):
+    def __init__(self, target=None, args=None, kwargs=None, daemon=False, **_):
         self.target = target
         self.daemon = daemon
-        self.kwargs = kwargs
+        self.args = args or ()
+        self.kwargs = kwargs or {}
         self._started = False

     def start(self):
         """Execute the stored target exactly once."""
         if not self._started and self.target:
             self._started = True
-            self.target()
+            self.target(*self.args, **self.kwargs)
🤖 Prompt for AI Agents
In `@tests/test_github_login.py` around lines 183 - 207, The ImmediateThread mock
drops thread arguments; update the ImmediateThread class (its __init__ and start
methods) to accept explicit args and kwargs parameters, store them on self
(e.g., self.args, self.kwargs) and call the target with those parameters in
start (self.target(*self.args, **self.kwargs)); keep existing behavior for
daemon, _started, join, and is_alive to preserve test semantics.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/switchcraft/gui_modern/views/group_manager_view.py (2)

450-450: Inconsistent dialog close pattern.

This line uses self.app_page.dialog which is the older Flet API and may not correctly reference the dialog opened via page.open(). The rest of the file (including the changed code) uses direct dialog references with self._close_dialog(dlg).

🔧 Suggested fix
-            actions=[ft.TextButton(i18n.get("btn_close") or "Close", on_click=lambda e: self._close_dialog(self.app_page.dialog))],
+            actions=[ft.TextButton(i18n.get("btn_close") or "Close", on_click=lambda e: self._close_dialog(dlg))],

397-397: Fix UI update in background thread (line 419 only).

add_dlg.update() at line 419 is called from a background thread spawned at line 421. Flet requires UI updates to be performed on the main event loop; calling .update() directly from background threads can cause race conditions. Use page.schedule_update() or page.run_task() to marshal the update back to the main thread:

def _bg():
    try:
        # ... fetch and populate results_list ...
    except Exception as ex:
        # ... handle error ...
    page.schedule_update()  # or: page.run_task(add_dlg.update)

(Line 397 is safe—it runs on the main thread within the event handler.)

🤖 Fix all issues with AI agents
In `@src/switchcraft/gui_modern/views/settings_view.py`:
- Around line 692-708: The error path in _init_flow currently mutates UI state
(setting loading_dlg.open = False and calling self.app_page.update()) and
invokes self._show_snack from a background thread; wrap those UI interactions in
the same main-thread helper used elsewhere (e.g., run_task) so they execute on
the UI thread: catch the exception in _init_flow and call run_task(lambda: (set
loading_dlg.open=False, self.app_page.update(), self._show_snack(...))) or an
equivalent callable that performs those three actions on the main thread, and do
the same for the early-return path where flow is falsy to ensure all
loading_dlg.open, self.app_page.update(), and self._show_snack calls run on the
UI thread.
- Around line 796-801: The code calls self.app_page.run_task(_close_and_result)
but _close_and_result is currently synchronous, while run_task expects an async
callable; convert _close_and_result to an async def (e.g., async def
_close_and_result(...)) and update any internal calls (await where needed) so it
returns an awaitable, then keep the run_task call as-is; ensure any synchronous
thread (_poll_token) that triggers run_task still calls it with the awaitable
and that dialog.close() and self.login_result assignment happen inside the async
function.
♻️ Duplicate comments (1)
src/switchcraft/gui_modern/views/settings_view.py (1)

808-821: Same run_task concern: lambda wrapper returns sync result.

The lambda: _show_dialog_with_flow(flow) wrapper creates a synchronous callable, which may not integrate correctly with run_task that expects async callables. The extensive fallback handling is good for resilience, but consider consolidating the async/sync approach.

🧹 Nitpick comments (3)
src/switchcraft/gui_modern/app.py (1)

1825-1826: Redundant assignment of page.switchcraft_app.

This assignment duplicates line 43 in __init__ where self.page.switchcraft_app = self is already set. Since main() creates a ModernApp which runs __init__, the attribute is already set by the time line 1826 executes.

The duplication is harmless but could be removed for clarity.

♻️ Optional: Remove redundant assignment
 def main(page: ft.Page):
     # Add restart method to page for injection if needed, or pass app instance.
     app = ModernApp(page)
     page._show_restart_countdown = app._show_restart_countdown
-    # Make app accessible to views via page
-    page.switchcraft_app = app
src/switchcraft/gui_modern/views/group_manager_view.py (1)

383-389: Remove unused instance attribute assignment.

self.dlg_add_member = add_dlg on line 389 is assigned but never referenced elsewhere in the codebase. The local add_dlg variable is correctly used in the closure-based callbacks, making the instance attribute redundant.

♻️ Suggested fix: remove unused assignment
             # Create dialog first so it can be referenced in nested functions
             add_dlg = ft.AlertDialog(
                 title=ft.Text(i18n.get("dlg_add_member") or "Add Member"),
                 content=ft.Column([search_box, results_list], height=300, width=400),
                 actions=[ft.TextButton(i18n.get("btn_close") or "Close", on_click=lambda e: self._close_dialog(add_dlg))]
             )
-            self.dlg_add_member = add_dlg
src/switchcraft/gui_modern/views/settings_view.py (1)

1058-1106: Consistent pattern with run_task fallbacks, but same async/sync concern applies.

The _reload_app() function follows the same pattern as the GitHub login flow with run_task and fallbacks. The same async/sync concern noted earlier applies here—_reload_app is synchronous but passed to run_task.

The extensive fallback handling ensures users always get feedback, which is good for resilience.

Consider defining a shared helper for this run_task-with-fallback pattern to reduce code duplication across _start_github_login and _on_lang_change:

Example helper pattern
def _run_on_main_thread(self, func):
    """Run a function on the main thread with fallback."""
    if hasattr(self.app_page, 'run_task'):
        try:
            # If func is sync, wrap it in async
            async def _wrapper():
                func()
            self.app_page.run_task(_wrapper)
            return
        except Exception as ex:
            logger.exception(f"run_task failed: {ex}")
    # Fallback: direct call
    try:
        func()
    except Exception as ex:
        logger.exception(f"Direct call failed: {ex}")

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/building.md`:
- Around line 141-147: Update the anchor link in the docs reference: in
docs/building.md change the SECURITY.md link target from
"SECURITY.md#-development-server-security" to
"SECURITY.md#development-server-security" so it matches the generated header
anchor (the header text "## ⚠️ Development Server Security" -> anchor
"#development-server-security"); ensure the link text remains intact and only
the anchor is corrected.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/switchcraft/gui_modern/utils/view_utils.py`:
- Around line 95-98: The two call sites in settings_view.py that invoke
_run_task_with_fallback inside exception handlers currently omit the error_msg
parameter, causing silent failures; update those invocations to pass an
informative error_msg (e.g., error_msg=f"Failed to {action}: {e}" or a
user-friendly string) so the fallback shows user-visible feedback, and ensure
you capture the caught exception variable (e) or otherwise format the message
before calling _run_task_with_fallback.

In `@src/switchcraft/gui_modern/views/group_manager_view.py`:
- Around line 418-422: The fallback calls a non-existent method
self.app_page.schedule_update(), causing AttributeError; change the fallback to
directly invoke the dialog update function instead—use add_dlg.update() when
self.app_page.run_task is not available so the code uses
self.app_page.run_task(add_dlg.update) if present and add_dlg.update() otherwise
(referencing add_dlg.update, self.app_page.run_task, and removing
schedule_update()).

In `@tests/test_template_company.py`:
- Around line 29-31: The test currently bails out of cleanup in CI
(is_ci_environment()) which can leave test_output.ps1 around and cause test
order-dependent false positives; instead, modify the cleanup logic in
tests/test_template_company.py so that when is_ci_environment() is true you skip
only the retry/delay logic but still perform a single attempt to remove
test_output.ps1 (i.e., call the same cleanup routine used for non-CI runs but
without exponential-backoff/retries), ensuring tests like
test_generate_without_company_name and test_generate_with_company_name call
generate() against a freshly cleaned state.
♻️ Duplicate comments (1)
tests/test_winget_details.py (1)

117-135: Make the loading-state assertion deterministic (avoid timing flakiness).

The time.sleep(0.3) approach is fragile across different environments. This was flagged in a previous review.

🧹 Nitpick comments (6)
tests/test_template_company.py (1)

5-17: Consider using relative import for conftest.

The import from conftest import is_ci_environment relies on pytest's special handling of conftest.py. When running tests directly (e.g., python tests/test_template_company.py), this import may fail since conftest isn't a standard module on the path. The fallback handles this gracefully.

Minor style note: stdlib imports (os) conventionally go before local/project imports per PEP 8.

Suggested improvement
 import unittest
+import os
 from pathlib import Path
 from unittest.mock import patch
 from switchcraft.utils.templates import TemplateGenerator
-import os

 # Import CI detection helper
 try:
-    from conftest import is_ci_environment
+    from tests.conftest import is_ci_environment
 except ImportError:
     # Fallback if conftest not available - mirror conftest.is_ci_environment() conditions
     def is_ci_environment():
         return (
             os.environ.get('CI') == 'true' or
             os.environ.get('GITHUB_ACTIONS') == 'true' or
             os.environ.get('GITHUB_RUN_ID') is not None
         )
src/switchcraft/gui_modern/app.py (1)

279-304: Multiple page.update() calls may cause UI flicker.

There are 5 page.update() calls in quick succession (lines 283, 287, 293, 301, 304). While defensive, this could cause noticeable flicker. Consider consolidating updates or using a single update after all state changes are applied.

♻️ Suggested consolidation
             # Set drawer on page FIRST
             self.page.end_drawer = drawer
-
-            # Force update to attach drawer to page
-            self.page.update()
-
-            # Now set open BEFORE updating (Flet needs this order)
+            # Set open state
             drawer.open = True
-            self.page.update()

             # Try additional methods if drawer didn't open
             try:
                 if hasattr(self.page, 'open'):
                     self.page.open(drawer)
-                    self.page.update()
             except Exception as ex:
                 logger.debug(f"page.open() not available or failed: {ex}, using direct assignment")

             # Final verification and update
             if not drawer.open:
                 logger.warning("Drawer open flag is False, forcing it to True")
                 drawer.open = True
-                self.page.update()

-            # Final update to ensure drawer is visible
+            # Single update after all state changes
             self.page.update()
tests/test_winget_details.py (1)

161-193: Refactor duplicate collect_text function.

The collect_text helper is defined twice within the same test (lines 165-172 for the polling condition and lines 183-190 for the final assertion). Extract this to a single nested function or use a closure to avoid duplication.

♻️ Suggested refactor
+    def collect_text(control, error_texts):
+        if isinstance(control, ft.Text):
+            error_texts.append(control.value)
+        elif hasattr(control, 'controls'):
+            for c in control.controls:
+                collect_text(c, error_texts)
+        elif hasattr(control, 'content'):
+            collect_text(control.content, error_texts)
+
     # Wait for error handling using polling
     def error_shown():
         if view.details_area is None:
             return False
         error_texts = []
-        def collect_text(control):
-            if isinstance(control, ft.Text):
-                error_texts.append(control.value)
-            elif hasattr(control, 'controls'):
-                for c in control.controls:
-                    collect_text(c)
-            elif hasattr(control, 'content'):
-                collect_text(control.content)
-        collect_text(view.details_area)
+        collect_text(view.details_area, error_texts)
         return any("error" in str(text).lower() or "failed" in str(text).lower() for text in error_texts)

     assert poll_until(error_shown, timeout=2.0), "Error should be shown within timeout"

     # Check that error was shown
     assert view.details_area is not None
     assert isinstance(view.details_area, ft.Column)
     # Should have error message
     error_texts = []
-    def collect_text(control):
-        if isinstance(control, ft.Text):
-            error_texts.append(control.value)
-        elif hasattr(control, 'controls'):
-            for c in control.controls:
-                collect_text(c)
-        elif hasattr(control, 'content'):
-            collect_text(control.content)
-
-    collect_text(view.details_area)
+    collect_text(view.details_area, error_texts)
src/switchcraft/gui_modern/views/settings_view.py (1)

746-782: Redundant dialog open check.

Lines 777-781 force dlg.open = True if the flag is False, but this happens immediately after attempting to open the dialog. If page.open() or manual assignment succeeded, the flag should already be True. This suggests a potential race or the dialog opening is unreliable.

Consider adding a small delay or verifying why the dialog might not be open after the display attempts, rather than forcing the flag.

tests/test_all_buttons.py (2)

63-140: Consider not storing view instances in the returned dictionary.

The collect_all_buttons function stores the entire view instance in the returned dictionary (line 122: 'view': view). This could lead to memory issues if views are large or hold references to resources. Since the view isn't used after button collection, consider removing it.

♻️ Suggested change
                 all_buttons[view_class.__name__] = {
                     'buttons': buttons,
                     'count': len(buttons),
-                    'view': view
                 }

230-241: Consider extracting mock_page setup to a helper or fixture.

The mock_page setup code is duplicated between collect_all_buttons (lines 74-86) and test_view_buttons_work (lines 230-241). Extract this to a shared fixture or helper function.

♻️ Suggested refactor
def create_mock_page():
    """Create a mock Flet page for testing."""
    mock_page = MagicMock(spec=ft.Page)
    mock_page.update = MagicMock()
    mock_page.switchcraft_app = MagicMock()
    mock_page.switchcraft_app.goto_tab = MagicMock()
    mock_page.switchcraft_app._current_tab_index = 0
    mock_page.dialog = None
    mock_page.end_drawer = None
    mock_page.snack_bar = None
    mock_page.open = MagicMock()
    mock_page.close = MagicMock()
    mock_page.run_task = lambda func: func()
    type(mock_page).page = property(lambda self: mock_page)
    return mock_page

Then use mock_page = create_mock_page() in both places.

Also applies to: 74-86

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/switchcraft/gui_modern/views/group_manager_view.py (1)

139-203: Marshal UI updates from the background thread.
Lines 144–203 update controls (_update_table, _show_snack, self.update, list_container.disabled) directly inside _bg(). Since _bg() runs in a daemon thread (spawned at line 203), these UI mutations should be marshaled to the page thread using app_page.run_task(). This pattern is used throughout the codebase (intune_view.py, winget_view.py, intune_store_view.py) and even within this file elsewhere (e.g., add_dlg.update). Without marshaling, you risk runtime errors or inconsistent UI state in Flet.

🛠️ Refactor approach
             self.filtered_groups = self.groups
-            try:
-                self._update_table()
-            except (RuntimeError, AttributeError):
-                logger.debug("Cannot update table: control not added to page")
+            def _apply_updates():
+                self._update_table()
+                self.list_container.disabled = False
+                self.update()
+            if hasattr(self.app_page, 'run_task'):
+                self.app_page.run_task(_apply_updates)
+            else:
+                _apply_updates()
...
-        else:
-            # Only update UI if no exception occurred
-            self.list_container.disabled = False
-            self.update()

Apply run_task marshaling to all UI updates in exception handlers as well.

src/switchcraft/gui_modern/views/settings_view.py (1)

1458-1484: run_task() expects an async callable, but _ui_update is synchronous.

Similar to the issue addressed in _close_and_result, Flet's run_task requires an async function. The synchronous _ui_update will not execute correctly.

Proposed fix
             # UI Update needs to be safe
-            def _ui_update():
+            async def _ui_update():
                 controls["btn"].visible = True
                 controls["progress"].visible = False
 
                 if success:
🤖 Fix all issues with AI agents
In `@src/switchcraft/gui_modern/views/intune_view.py`:
- Around line 206-216: Replace the overly broad "except BaseException" with
"except Exception" and consolidate the redundant exception handlers: capture the
error string as is done with error_msg, call the existing update_error closure
(or a renamed UI update function that sets self.up_status.value/color and calls
self.update()) via self.app_page.run_task to ensure the UI shows the failure,
and log the exception with logger.exception within the same Exception handler
(use update_error and logger.exception together inside the Exception block so
KeyboardInterrupt/SystemExit are not caught and the UI is never left in
"Connecting..." state).
- Around line 621-628: The code calls subprocess.run(...) and then attempts
proc.wait(...), but subprocess.run returns a CompletedProcess with no wait
method; remove the try/except block that calls proc.wait and any related
comments so the call simply uses subprocess.run(['explorer',
f'/select,{safe_path}']) (or if you really need non-blocking behavior, replace
subprocess.run with subprocess.Popen and then call proc.wait(timeout=5.0)
handling subprocess.TimeoutExpired). Update around the proc variable and the
surrounding dlg.open = False / self.app_page.update() so there is no call to
proc.wait.

In `@src/switchcraft/gui_modern/views/settings_view.py`:
- Around line 704-713: The closure _handle_error captures the exception variable
ex which can be out of scope when _run_task_with_fallback runs; change
_handle_error to capture the value by using a default parameter (e.g., def
_handle_error(exc=ex): ...) or reference the already-built error_msg instead,
then use that captured value inside the body to set loading_dlg.open = False,
call self.app_page.update(), and call self._show_snack with the captured message
before passing _handle_error to _run_task_with_fallback.

In `@tests/test_all_buttons.py`:
- Around line 41-58: The current assertion only checks that
collect_all_buttons() returned entries but not that any instantiated
successfully; update test_all_views_have_buttons to assert at least one
successful instantiation by scanning all_buttons (from collect_all_buttons())
for entries without an 'error' key (e.g., build a list of successes where
'error' not in info) and assert len(successes) > 0 with a clear failure message
indicating no views instantiated successfully.

In `@tests/test_loading_screen.py`:
- Around line 10-49: In test_loading_screen_is_displayed remove the broad
"except Exception" swallow so unexpected initialization errors surface; only
catch SystemExit around the call to main(mock_page) (the test function and
ModernApp patch stay the same) and let any other exceptions propagate and fail
the test—update the try/except to catch only SystemExit and delete the generic
Exception handler so real regressions aren't hidden.
🧹 Nitpick comments (7)
src/switchcraft/gui_modern/app.py (2)

134-143: De-duplicate splash termination and clear the handle after success.
The same terminate/wait/kill block runs in both __init__ and build_ui. Consider centralizing it (or at least nulling self.splash_proc) to avoid double termination attempts and future drift.

♻️ Minimal tweak (apply in both blocks)
             try:
                 self.splash_proc.wait(timeout=1.0)
             except Exception:
                 # If wait fails, try kill as fallback
                 try:
                     self.splash_proc.kill()
                 except Exception:
                     pass
+            # Prevent double-terminate attempts later
+            self.splash_proc = None

Also applies to: 1028-1037


198-209: Consider a generic, localized user message here.
Surface a friendly message in the SnackBar and keep the raw exception in logs to avoid leaking internal details to users.

✅ Suggested tweak
-    self.page.snack_bar = ft.SnackBar(
-        content=ft.Text(f"Failed to open notifications: {ex}"),
-        bgcolor="RED"
-    )
+    self.page.snack_bar = ft.SnackBar(
+        content=ft.Text(i18n.get("notif_open_failed") or "Failed to open notifications."),
+        bgcolor="RED"
+    )
src/switchcraft/modern_main.py (1)

410-420: Prefer list args for explorer launch to avoid quoting edge cases.
Line 412 builds a command string; using list args is more robust for paths with spaces/quotes and avoids command-line parsing quirks.

🛠️ Suggested tweak
-            proc = subprocess.Popen(f'explorer "{dump_folder}"')
+            proc = subprocess.Popen(["explorer", dump_folder])
src/switchcraft/gui_modern/utils/view_utils.py (1)

87-146: Handle coroutine fallbacks to avoid silently skipping async tasks.
If any callers pass async def functions, the direct fallback will return a coroutine without executing it. Consider detecting coroutine functions and running/scheduling them.

🛠️ Suggested pattern
     def _run_task_with_fallback(self, task_func, fallback_func=None, error_msg=None):
+        import asyncio
+        def _invoke(func):
+            if asyncio.iscoroutinefunction(func):
+                try:
+                    loop = asyncio.get_running_loop()
+                except RuntimeError:
+                    asyncio.run(func())
+                else:
+                    loop.create_task(func())
+            else:
+                func()
         ...
                 try:
-                    fallback_func()
+                    _invoke(fallback_func)
                     return True
                 except Exception as ex2:
                     ...
         else:
             # No run_task, try direct call
             try:
-                fallback_func()
+                _invoke(fallback_func)
                 return True
tests/test_all_buttons.py (1)

211-295: Consider async-safe handler invocation.
If any on_click handlers are async def, calling them directly won’t execute the coroutine. You may want to detect coroutine handlers and run them via an event loop.

🛠️ Suggested pattern
+    import asyncio
...
-                    button.on_click(mock_event)
+                    if asyncio.iscoroutinefunction(button.on_click):
+                        asyncio.run(button.on_click(mock_event))
+                    else:
+                        button.on_click(mock_event)
                     successes += 1
src/switchcraft/gui_modern/views/settings_view.py (2)

802-823: Simplify the fallback path; the asyncio.create_task branch is unreachable.

When run_task is unavailable in a background thread, asyncio.get_running_loop() will raise RuntimeError (no running loop), so line 809 is never reached. The logic can be simplified to go directly to the synchronous fallback.

Proposed simplification
                     if hasattr(self.app_page, 'run_task'):
                         self.app_page.run_task(_close_and_result)
                     else:
-                        # Fallback: execute synchronously if run_task not available
-                        # Note: This is not ideal but provides backward compatibility
-                        import asyncio
-                        try:
-                            loop = asyncio.get_running_loop()
-                            # If loop is running, schedule the coroutine
-                            asyncio.create_task(_close_and_result())
-                        except RuntimeError:
-                            # No event loop available, create one
-                            try:
-                                asyncio.run(_close_and_result())
-                            except Exception:
-                                # Last resort: try to execute the logic directly
-                                dlg.open = False
-                                self.app_page.update()
-                                if token:
-                                    AuthService.save_token(token)
-                                    self._update_sync_ui()
-                                    self._show_snack(i18n.get("login_success") or "Login Successful!", "GREEN")
-                                else:
-                                    self._show_snack(i18n.get("login_failed") or "Login Failed or Timed out", "RED")
+                        # Fallback: execute synchronously if run_task not available
+                        dlg.open = False
+                        self.app_page.update()
+                        if token:
+                            AuthService.save_token(token)
+                            self._update_sync_ui()
+                            self._show_snack(i18n.get("login_success") or "Login Successful!", "GREEN")
+                        else:
+                            self._show_snack(i18n.get("login_failed") or "Login Failed or Timed out", "RED")

824-826: Consider catching Exception instead of BaseException.

Catching BaseException also catches SystemExit and KeyboardInterrupt, which could interfere with clean shutdown. Since this is a daemon thread, it will terminate when the main thread exits anyway.

Suggested change
-                except BaseException:
-                    # Catch all exceptions including KeyboardInterrupt to prevent unhandled thread exceptions
+                except Exception:
+                    # Catch exceptions to prevent unhandled thread errors
                     logger.exception("Unexpected error in token polling background thread")

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/switchcraft/gui_modern/utils/view_utils.py`:
- Around line 127-149: The code calls page.run_task(task_func) without ensuring
task_func is an awaitable; check whether task_func is async (use
inspect.iscoroutinefunction(task_func) or inspect.isawaitable when appropriate)
before calling page.run_task, and if it's synchronous call
page.run_thread(task_func) instead; update the branch that currently uses
page.run_task to choose page.run_task(task_func) for coroutine functions and
page.run_thread(task_func) for sync functions, keeping the existing fallback
logic that uses is_fallback_coroutine and fallback_func intact and preserving
the existing exception handling and snack display.

In `@src/switchcraft/gui_modern/views/group_manager_view.py`:
- Around line 305-307: Replace the overly broad except BaseException: in the
group creation background thread with except Exception as e: to avoid catching
KeyboardInterrupt/SystemExit; update the handler that calls
logger.exception("Unexpected error in group creation background thread") to use
the captured exception (e) so the logged message includes the actual exception
context and stack trace; locate the block containing the logger.exception call
in the group creation background thread and make this change there.
- Around line 207-220: The except block is too broad—replace "except
BaseException as be:" with "except Exception as e:" so we don't swallow
KeyboardInterrupt/SystemExit/GeneratorExit; keep the existing
logger.exception("Unexpected error in group loading background thread",
exc_info=e) (or simply logger.exception(...) as currently used) and leave the UI
marshalling logic (the nested update_ui function, calls to
self.list_container.disabled, self.update(), and the self.app_page.run_task
path) intact so only ordinary exceptions are caught and logged while fatal
system exceptions propagate.

In `@tests/test_all_buttons.py`:
- Around line 284-290: The test invokes async handlers via
asyncio.create_task(handler(mock_event)) which can swallow exceptions; change
the invocation to ensure the coroutine is awaited and exceptions surface: detect
running loop as before but replace asyncio.create_task(...) with awaiting the
handler (e.g., await handler(mock_event)) when inside an async test helper, or
capture and await the created task (await
asyncio.create_task(handler(mock_event))) or consistently call
asyncio.run(handler(mock_event)) in the non-running-loop branch so that
handler(mock_event) is always awaited; update the branches around loop, handler,
mock_event, asyncio.create_task, and asyncio.run accordingly.
♻️ Duplicate comments (1)
tests/test_github_login.py (1)

183-207: ImmediateThread mock doesn't forward args parameter.

Per past review feedback, production code uses threading.Thread(target=func, args=(value,)). The current mock captures kwargs but not args, so threaded functions expecting positional arguments will fail silently.

Suggested fix from past review
 class ImmediateThread:
     """Thread-like mock that executes target immediately when start() is called."""
-    def __init__(self, target=None, daemon=False, **kwargs):
+    def __init__(self, target=None, args=None, kwargs=None, daemon=False, **_):
         self.target = target
         self.daemon = daemon
-        self.kwargs = kwargs
+        self.args = args or ()
+        self.kwargs = kwargs or {}
         self._started = False

     def start(self):
         """Execute the stored target exactly once."""
         if not self._started and self.target:
             self._started = True
-            self.target()
+            self.target(*self.args, **self.kwargs)
🧹 Nitpick comments (7)
src/switchcraft/gui_modern/utils/view_utils.py (2)

119-125: Unused variable is_coroutine detected.

is_coroutine is computed on line 124 but never used. The code only checks is_fallback_coroutine. If the intent was to handle task_func as async when passed to run_task, note that Flet's run_task already handles both sync and async callables.

Remove unused variable
-        # Check if task_func is a coroutine function
-        import asyncio
-        is_coroutine = asyncio.iscoroutinefunction(task_func)
+        import asyncio
         is_fallback_coroutine = asyncio.iscoroutinefunction(fallback_func) if fallback_func else False

137-141: Unused variable loop in async fallback paths.

The variable loop is assigned from asyncio.get_running_loop() but never used—only asyncio.create_task() is called. This works but is misleading.

Simplify async handling
                     try:
-                        loop = asyncio.get_running_loop()
+                        asyncio.get_running_loop()  # Check if loop exists
                         asyncio.create_task(fallback_func())
                     except RuntimeError:
                         asyncio.run(fallback_func())

Also applies to: 155-159

src/switchcraft/gui_modern/views/group_manager_view.py (1)

163-172: Consider using _run_task_with_fallback helper.

The ViewMixin provides _run_task_with_fallback (from view_utils.py) specifically to consolidate this pattern. These error-handling paths could be simplified.

Example refactor for one error path
             except requests.exceptions.HTTPError as e:
                 # ... error message setup ...
-                # Marshal UI update to main thread
-                def show_error():
-                    try:
-                        self._show_snack(error_msg, "RED")
-                    except (RuntimeError, AttributeError):
-                        pass  # Control not added to page (common in tests)
-                if hasattr(self.app_page, 'run_task'):
-                    self.app_page.run_task(show_error)
-                else:
-                    show_error()
+                self._run_task_with_fallback(
+                    lambda: self._show_snack(error_msg, "RED"),
+                    error_msg=None  # Already showing error via snack
+                )

Also applies to: 177-186, 197-206

tests/test_all_buttons.py (1)

302-304: Test may pass with all handlers failing if at least one has no handler.

If a button has on_click=None, it doesn't enter the try block, so successes isn't incremented but failures aren't counted either. The assertion successes > 0 could fail misleadingly. Consider tracking buttons processed vs. buttons with handlers.

tests/test_github_login.py (2)

69-72: Consider using unittest.mock.patch instead of direct attribute assignment.

The delayed_poll side effect works, but using mock_auth_service.return_value.poll_for_token.side_effect pattern would be cleaner for instance method mocking.


139-147: Redundant CI check.

Lines 145-146 manually check CI environment variables, but skip_if_ci() is available and already imported. Consider using it for consistency.

Use skip_if_ci for consistency
 def test_github_login_success_saves_token(mock_page, mock_auth_service):
     """Test that successful GitHub login saves token and updates UI."""
     from switchcraft.gui_modern.views.settings_view import ModernSettingsView
     import threading

-    # Skip in CI to avoid long waits
-    if os.environ.get('CI') == 'true' or os.environ.get('GITHUB_ACTIONS') == 'true':
-        pytest.skip("Skipping test with time.sleep in CI environment")
+    skip_if_ci("Test uses time.sleep and threading, not suitable for CI")
src/switchcraft/gui_modern/views/settings_view.py (1)

820-822: Bare except Exception: catches too broadly.

The comment says "Catch all exceptions including KeyboardInterrupt" but the code catches Exception, not BaseException. This is actually correct, but the comment is misleading.

Fix misleading comment
-                except Exception:
-                    # Catch all exceptions including KeyboardInterrupt to prevent unhandled thread exceptions
+                except Exception:
+                    # Catch exceptions to prevent unhandled thread exceptions
                     logger.exception("Unexpected error in token polling background thread")

Comment on lines +207 to +220
except BaseException as be:
# Catch all exceptions including KeyboardInterrupt to prevent unhandled thread exceptions
logger.exception("Unexpected error in group loading background thread")
# Marshal UI update to main thread
def update_ui():
try:
self.list_container.disabled = False
self.update()
except (RuntimeError, AttributeError):
pass
if hasattr(self.app_page, 'run_task'):
self.app_page.run_task(update_ui)
else:
update_ui()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Catching BaseException is overly broad.

Catching BaseException includes KeyboardInterrupt, SystemExit, and GeneratorExit, which typically shouldn't be suppressed. Consider catching Exception instead and letting the truly exceptional cases propagate.

Suggested fix
-            except BaseException as be:
-                # Catch all exceptions including KeyboardInterrupt to prevent unhandled thread exceptions
+            except Exception as be:
+                # Catch remaining exceptions to prevent unhandled thread exceptions
                 logger.exception("Unexpected error in group loading background thread")
🤖 Prompt for AI Agents
In `@src/switchcraft/gui_modern/views/group_manager_view.py` around lines 207 -
220, The except block is too broad—replace "except BaseException as be:" with
"except Exception as e:" so we don't swallow
KeyboardInterrupt/SystemExit/GeneratorExit; keep the existing
logger.exception("Unexpected error in group loading background thread",
exc_info=e) (or simply logger.exception(...) as currently used) and leave the UI
marshalling logic (the nested update_ui function, calls to
self.list_container.disabled, self.update(), and the self.app_page.run_task
path) intact so only ordinary exceptions are caught and logged while fatal
system exceptions propagate.

Comment on lines +305 to +307
except BaseException:
# Catch all exceptions including KeyboardInterrupt to prevent unhandled thread exceptions
logger.exception("Unexpected error in group creation background thread")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same BaseException concern in group creation.

This pattern appears again. Consider using Exception instead.

🤖 Prompt for AI Agents
In `@src/switchcraft/gui_modern/views/group_manager_view.py` around lines 305 -
307, Replace the overly broad except BaseException: in the group creation
background thread with except Exception as e: to avoid catching
KeyboardInterrupt/SystemExit; update the handler that calls
logger.exception("Unexpected error in group creation background thread") to use
the captured exception (e) so the logged message includes the actual exception
context and stack trace; locate the block containing the logger.exception call
in the group creation background thread and make this change there.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/switchcraft/gui_modern/utils/view_utils.py`:
- Around line 137-160: The file references the asyncio module (e.g., in the try
blocks that call asyncio.get_running_loop(), asyncio.create_task(), and
asyncio.run() while handling fallback_func and is_fallback_coroutine) but does
not import it; add "import asyncio" at the top of
src/switchcraft/gui_modern/utils/view_utils.py so the coroutine fallback paths
(those using asyncio.get_running_loop, asyncio.create_task, asyncio.run) do not
raise NameError; ensure the import follows project import ordering/style.

In `@tests/test_all_buttons.py`:
- Around line 269-271: The test currently asserts every view has at least one
button causing failure for DashboardView which legitimately has none; modify the
assertion in tests/test_all_buttons.py to allow zero buttons for display-only
views by either skipping/excluding "DashboardView" from the non-empty check or
making the check conditional (e.g., if view_name != "DashboardView": assert
len(buttons) > 0) — reference the variables buttons and view_name and the view
name "DashboardView" to locate where to apply the conditional.

In `@tests/test_all_three_issues.py`:
- Around line 7-12: The test file is missing an asyncio import causing NameError
when run_task inside the mock_page fixture uses asyncio.run,
asyncio.get_running_loop, and asyncio.create_task; fix by adding "import
asyncio" to the top imports so run_task and the mock_page fixture can reference
asyncio symbols (look for the mock_page fixture and the run_task function that
call asyncio.run/get_running_loop/create_task).
♻️ Duplicate comments (2)
src/switchcraft/gui_modern/utils/view_utils.py (1)

122-130: Use run_task only for coroutines (avoid exception-driven flow).

is_coroutine is computed but not used; page.run_task(task_func) will throw for sync functions and then rely on fallback. Prefer branching to run_task vs run_thread (or direct call) up front.

♻️ Suggested adjustment
-            try:
-                page.run_task(task_func)
+            try:
+                if is_coroutine:
+                    page.run_task(task_func)
+                elif hasattr(page, "run_thread"):
+                    page.run_thread(task_func)
+                else:
+                    task_func()
                 return True
Flet Page.run_task vs Page.run_thread: when should each be used and what input types do they accept?
tests/test_all_buttons.py (1)

286-295: Async handler invocation may silently swallow errors.

When asyncio.create_task() is used without awaiting the result, any exception raised by the handler won't fail the test. This was flagged in a previous review.

🐛 Suggested fix
                     if inspect.iscoroutinefunction(handler):
                         # Handler is async, need to run it
-                        try:
-                            loop = asyncio.get_running_loop()
-                            asyncio.create_task(handler(mock_event))
-                        except RuntimeError:
-                            asyncio.run(handler(mock_event))
+                        asyncio.run(handler(mock_event))
                     else:
                         handler(mock_event)
🧹 Nitpick comments (3)
.github/workflows/ci.yml (1)

29-42: Consider adding workflow_dispatch to the 3.15 condition for manual runs.

The current condition runs Python 3.15 only on push to main. With workflow_dispatch triggers, 3.15 jobs will be skipped. If manual testing of 3.15 is desirable, consider extending the condition.

♻️ Optional enhancement
         if [[ "${{ matrix.python-version }}" == "3.14" ]] || \
            ([[ "${{ matrix.python-version }}" == "3.15" ]] && \
             [[ "${{ github.event_name }}" == "push" ]] && \
-            [[ "${{ github.ref }}" == "refs/heads/main" ]]); then
+            [[ "${{ github.ref }}" == "refs/heads/main" ]]) || \
+           ([[ "${{ matrix.python-version }}" == "3.15" ]] && \
+            [[ "${{ github.event_name }}" == "workflow_dispatch" ]]); then
tests/test_all_buttons.py (2)

26-40: The elif chain may miss buttons in controls with multiple child containers.

If a control has both controls and content attributes, only controls is searched. Consider checking all possible child attributes to ensure complete button discovery.

♻️ Proposed improvement
 def find_all_buttons_in_control(control, buttons_found):
     """Recursively find all buttons in a control tree."""
     if isinstance(control, (ft.Button, ft.IconButton, ft.TextButton, ft.ElevatedButton, ft.OutlinedButton, ft.FilledButton)):
         buttons_found.append(control)

     # Check if control has children/controls
     if hasattr(control, 'controls'):
         for child in control.controls:
             find_all_buttons_in_control(child, buttons_found)
-    elif hasattr(control, 'content'):
+    if hasattr(control, 'content') and control.content is not None:
         find_all_buttons_in_control(control.content, buttons_found)
-    elif hasattr(control, 'actions') and control.actions:
+    if hasattr(control, 'actions') and control.actions:
         for action in control.actions:
             find_all_buttons_in_control(action, buttons_found)

86-95: Consider dynamic view discovery to avoid maintaining a hardcoded list.

The hardcoded view_files list may become stale as new views are added. Dynamic discovery would be more maintainable.

♻️ Optional: Dynamic discovery
def collect_all_buttons():
    """Helper function to collect all buttons from all views."""
    views_dir = os.path.join(os.path.dirname(__file__), '..', 'src', 'switchcraft', 'gui_modern', 'views')
    
    # Dynamically discover view files
    view_files = []
    if os.path.exists(views_dir):
        for f in os.listdir(views_dir):
            if f.endswith('_view.py') and not f.startswith('__'):
                view_files.append(f[:-3])  # Remove .py extension
    # ... rest of function

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/ci-orchestrator.yml (1)

80-85: Change issues: read to issues: write so the skip comment can be posted.

The workflow uses gh pr comment at line 105 to post a comment explaining why auto-merge was skipped. The gh pr comment command requires issues: write permission; with the current issues: read, the command will fail with a "Resource not accessible by integration" error and cause the job to fail.

🔧 Proposed fix
     permissions:
       contents: write
       pull-requests: write
       checks: read
-      issues: read
+      issues: write
🤖 Fix all issues with AI agents
In @.github/workflows/ci-orchestrator.yml:
- Around line 105-109: The multi-line message passed to the gh CLI in the gh pr
comment call is wrapped in double quotes which breaks YAML parsing; change the
command invocation that constructs the body (the gh pr comment "$PR_NUMBER"
--body "...") to use a shell heredoc/EOF style literal for the --body argument
so the entire multi-line text is preserved and YAML indentation isn't lost, e.g.
replace the quoted multi-line string with a heredoc form that feeds the message
to --body.

In @.github/workflows/review-auto-merge.yml:
- Around line 37-41: The multi-line literal in the gh pr comment step is
breaking YAML parsing; change the step to build the comment body with a heredoc
in the shell and then pass it to gh, e.g. create a BODY variable via a here-doc
(<<'EOF') in the run block and call gh pr comment "$PR_NUMBER" --body "$BODY" so
that the YAML stays valid; update the step that runs the gh pr comment command
(referenced as gh pr comment and PR_NUMBER in the diff) to use the heredoc
approach.

In `@src/switchcraft/gui_modern/utils/view_utils.py`:
- Around line 149-158: The sync branch incorrectly calls fallback_func()
unconditionally; change it so it mirrors the async branch by calling task_func()
first, and only if task_func() raises an exception invoke fallback_func() (and
log via logger.exception and show the error snack via self._show_snack if
error_msg is set). Update the block handling is_coroutine == False to call
task_func(), catch exceptions, log with logger.exception, then call
fallback_func() as the recovery path and return False on failure.
🧹 Nitpick comments (3)
src/switchcraft/gui_modern/utils/view_utils.py (2)

124-126: Consider moving import inspect to the module level.

The inspect import is inside the method body. While lazy imports can reduce startup time for expensive modules, inspect is a lightweight stdlib module. Moving it to the top of the file alongside other imports would improve readability and follow Python conventions.


137-148: Fire-and-forget tasks may silently lose exceptions.

The asyncio.create_task(fallback_func()) calls at lines 140 and 166 create tasks without storing references. If fallback_func() raises an exception after the task is created, it won't be caught by the surrounding try/except and may only appear in asyncio's default exception handler logs.

This pattern matches the test fixtures in tests/conftest.py, so it's consistent with the codebase. Consider storing the task reference and adding exception handling if reliability is critical.

tests/test_all_three_issues.py (1)

160-165: Prefer polling over fixed sleep to reduce flakiness.

You already have poll_until; use it here instead of time.sleep(0.5) to avoid timing-sensitive failures.

♻️ Suggested refactor
-    # Wait a bit for any background operations
-    time.sleep(0.5)
-
-    # Check that app was reloaded
-    assert mock_page.switchcraft_app.goto_tab.called, "goto_tab should be called to reload UI"
+    from conftest import poll_until
+    assert poll_until(
+        lambda: mock_page.switchcraft_app.goto_tab.called,
+        timeout=2.0
+    ), "goto_tab should be called to reload UI"

@github-actions
Copy link
Contributor

🚫 Auto-Merge Skipped

This PR has the no-auto-merge label. Auto-merge was not executed.

To enable auto-merge, please remove the no-auto-merge label.

@FaserF FaserF merged commit 1795b2a into main Jan 19, 2026
14 checks passed
@FaserF FaserF deleted the bug-fixes-new-feat branch January 19, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend ci-cd documentation Improvements or additions to documentation no-auto-merge Exclude a PR from the auto-merge workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants